Den Ballast selbstgemachter "dynamischer Programmbibliotheken" loswerden

--- | 13 Mai, 2016


Schreiben Sie immer noch ihre eigenen DLL?
Dann sind Sie hier richtig!


Einleitung

Irgendwann ist es immer so weit, dass der Funktionsumfang der Programmiersprache dem MQL5-Programmierer nicht mehr genügt und er sich anderer Hilfsmittel bedienen muss. Sei es, dass er mit einer Datenbank arbeiten oder Programmschnittstellen verwenden oder auf Funktionen des jeweiligen Betriebssystems zurückgreifen muss. Zur Erweiterung der Möglichkeiten des verwendeten MQL5-Programms ist er gezwungen, sich mit verschiedenen Programmierschnittstellen (API) zu befassen. Aber aus verschiedenen Gründen kann er nicht unmittelbar aus MQL5 über die benötigten Funktionen verfügen, da er weder weiß,

  • wie die komplexe Datenart (z. B. eine Struktur) in eine API-Funktion übersetzt wird, noch
  • wie mit dem von der API-Funktion ausgegebenen Adresszeiger umzugehen ist.

Deshalb ist er genötigt, sich einer anderen Programmiersprache zu bedienen sowie eine zwischengelagerte DLL mit dem für die Arbeit erforderlichen Funktionsumfang anzulegen. Obwohl MQL5 die Möglichkeit bietet, unterschiedliche Datenarten abzubilden und sie an eine Programmierschnittstelle (API) weiterzugeben, ist diese Programmiersprache nicht in der Lage, das Problem des Auszugs von Daten aus einer zugelassenen Speicheradresse zu lösen.

In diesem Beitrag wollen wir auf jedes „i“ einen Punkt setzen und einige einfache Mechanismen für die Übertragung und den Empfang komplexer Datenarten sowie für die Arbeit mit den ausgegebenen Speicheradressen vorstellen.


Inhalt

1. Speicherplatz - unser Ein und Alles

  • Erfassen der Speicheradressen
  • Kopieren der Speicherbereiche

2. Übertragung der Gerüste/Strukturen an die API-Funktionen

  • Umwandeln der Gerüste/Strukturen mit den in MQL5 vorhandenen Mitteln
  • Beispielhafte Übertragung eines Datengerüsts für Programmschnittstellen

3. Arbeiten mit Speicheradressen aus API-Funktionen

  • Beispielhafte Speicherplatzzuordnungsdateien (Memory Mapping File)
  • Ein Beispiel für MySQL

4. Auslesen der auf NULL endenden Zeichenfolgen aus API-Funktionen



1. Speicherplatz - unser Ein und Alles

Bekanntermaßen verfügt jede Variable (einschließlich solcher komplexer Datenarten) über eine konkrete Adresse, unter der sie im Speicher abgelegt wird. Bei dieser Adresse handelt es sich um eine ganze Vierbyte-Zahl (der Art „int“), deren Wert gleich der Adresse des ersten Bytes dieser Adresse ist.

Sobald alles vollständig festgelegt worden ist, kann mit besagtem Speicherbereich gearbeitet werden. In der Bibliothek der Programmiersprache C (msvcrt.dll) gibt es die Funktion memcpy. Sie ersetzt das fehlende Element, durch das MQL5 und die unterschiedlichen API-Bibliotheken zu einem Ganzen verbunden werden. Zudem verschafft sie dem Programmierer umfassende Möglichkeiten.


Greifen wir auf das Wissen unserer Ahnen zurück

Die Funktion Memcpy kopierte die angegebene Anzahl Bytes aus einem Zwischenspeicher (Puffer) in einen anderen und gibt die Speicheradresse für den empfangenden Zwischenspeicher aus.

void *memcpy(void *dst, const void *src, int cnt);
dst - pointer to the receiver buffer
src - pointer to the source buffer
cnt - number of bytes for copying

Anders gesagt: der mit der Adresse src beginnende cnt-Byte große Speicherbereich wird in den mit der Adresse dst beginnenden Speicherbereich kopiert.

Die unter der Adresse src befindlichen Daten können ganz unterschiedlich sein. Es kann sich um ein Byte umfassende Variablen der Art char, um eine aus acht Bytes bestehende Ziffer der Art double, ein Datenfeld (Array), irgendein Datengerüst oder einen beliebigen Speicherbereich handeln. Das bedeutet, dass , wenn die Adresse und die Größe bekannt sind, die Übertragung von Daten aus einem Speicherbereich in einen anderen problemlos ausgeführt werden kann.


Wie funktioniert das?

In der Abbildung 1 werden die ungefähren Größen einiger Datenarten im Vergleich zueinander dargestellt.

Die Größen unterschiedlicher Datenarten in MQL5


Der Zweck der Funktion Memcpy besteht darin, Daten aus einem Speicherbereich in einen anderen zu kopieren.
In Abbildung 2 ist der Kopiervorgang für vier Bytes zu sehen.

Beispiel für das Kopieren von 4 Bytes mittels memcpy

In MQL5 sieht das wie folgt aus:

Example 1. Using memcpy
#import "msvcrt.dll"
  int memcpy(int &dst, int &src, int cnt);
#import//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
  int dst, src=4, cnt=sizeof(int);
  int adr=memcpy(dst, src, cnt);
  Print("dst value="+string(dst)+"   Address dst="+string(adr));
}

Es darf nicht vergessen werden, dass es sich bei dem Speicherplatz, der von dst und src ausgewiesen wird, um völlig andere Datenarten handeln kann (Hauptsache ihre Größe entspricht der von cnt). Die Speicheradresse src kann sich beispielsweise auf eine Variable der Art double (mit cnt = 8 Byte) beziehen, während dst auf ein Datenfeld vergleichbarer Größe, char[8] oder int[2], verweist.

Für den Speicherplatz ist es unerheblich, welche Vorstellung der Programmierer in diesem Augenblick von ihm hat. Gleich, ob es sich um das Datenfeld (Array) char[8] oder lediglich um eine Variable der Art long oder ein Datengerüst der Art { int a1; int a2; } handeln soll.

Das heißt, es können nicht nur gleichartige Daten untereinander kopiert werden. So können wir zum Beispiel ein Datenfeld aus fünf Bytes in ein Datengerüst der Art {int i; char c;} übertragen und umgekehrt. Eben diese Beziehung ermöglicht die unmittelbare Arbeit mit API-Funktionen.

Wir betrachten die Verwendungsmöglichkeiten der Funktion memcpy der Reihe nach.


Erfassen der Speicheradressen

Im ersten Beispiel haben wir gezeigt, dass die Funktion memcpy die Adresse der Variablen dst ausgibt.

Diese Eigenschaft kann zur Erfassung der Adresse jeder beliebigen Variablen (einschließlich derjenigen von Datenfeldern und weiteren komplexen Datenarten) genutzt werden. Es reicht, ein und dieselbe Variable sowohl als Quelle als auch als Empfänger auszuweisen. An cnt können wir „0“ weitergeben, da der tatsächliche Kopiervorgang nicht zwingend ausgeführt werden muss.

Wir erhalten beispielsweise die Adresse für eine Variable der Art double und ein Datenfeld der Art short:

Example 2. Getting pointers to the variable
#import "msvcrt.dll"
  int memcpy(short &dst[], short &src[], int cnt);
  int memcpy(double &dst,  double &src, int cnt);
#import

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
  short src[5];
  //--- getting src array address (i.е., the address of the first element)
  int adr=memcpy(src, src, 0);
  double var;
  //--- getting var variable address
  adr=memcpy(var, var, 0); 
}

Die erfasste Adresse kann anschließend entweder als Gerüstparameter oder als Parameter eben derselben Funktion memcpy an die gewünschte API-Funktion weitergegeben werden.


Kopieren von Datenfeldern

Wie wir wissen, handelt es sich bei einem Datenfeld (Array) um einen abgegrenzten Speicherplatzbereich. Die Größe des abgegrenzten Speicherplatzes hängt von der Art und der Anzahl der zu speichernden Elemente ab. Mit 10 Elementen der Art short nimmt ein solches Datenfeld beispielsweise Speicherplatz im Umfang von 20 Byte ein (da short eine Größe von 2 Byte aufweist).

Diese 20 Byte könnten jedoch auch von einem Datenfeld aus 20 Elementen der Art char oder aus 5 der Art int belegt sein. Wie dem auch sei, im Speicher sind 20 Byte belegt.

Zum Kopieren der Datenfelder sind erforderlich:

  • die Abgrenzung der erforderlichen Anzahl Elemente für den Speicher dst (mindestens die Anzahl der resultierenden cnt Byte);
  • die Festlegung der Anzahl der aus src zu kopierenden Bytes in cnt.
Example 3. Copying the arrays
#import "msvcrt.dll"
  int memcpy(double &dst[],  double &src[], int cnt);
#import

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
  double src[5];
  //--- calculating the number of bytes!!!
  int cnt=sizeof(double)*ArraySize(src);
  double dst[]; 
  ArrayResize(dst, 5);
  //--- the array has been copied from src to dst
   memcpy(dst, src, cnt); }



2. Übertragung der Gerüste/Strukturen an die API-Funktionen

Angenommen, wir müssen ein gefülltes Datengerüst an eine API-Funktion weitergeben. Die Programmiersprache MQL5 erlegt uns bei der Übertragung von Datengerüsten Beschränkungen auf. Zu Beginn dieses Beitrages wurde festgestellt, dass es unterschiedliche Vorstellungen von dem Speicherplatz geben kann. Das bedeutet, dass jedes gewünschte Gerüst in jede von MQL5 unterstützte Datenart kopiert werden kann. Üblicherweise handelt es sich bei der für Datengerüste oder -strukturen geeigneten Art um ein Datenfeld (Array). Deshalb müssen wir aus dem Gerüst zunächst ein Datenfeld machen, das wir dann an die API-Funktion weitergeben.

In der Rubrik Dokumente wird eine Variante zum Kopieren von Speicherinhalten mittels Datengerüsten vorgestellt. Da wir hier Gerüste nicht als Parameter übertragen können, kommt die Verwendung der Funktion memcpy nicht infrage, und uns bleibt nur das Kopieren der Gerüste.

In der Abbildung 3 sehen wir die Darstellung eines Datengerüstes aus 5 Variablen unterschiedlicher Art sowie dessen Entsprechung in Form eines Datenfeldes der Art char.

Darstellung eines Datengerüstes aus 5 Variablen unterschiedlicher Art sowie dessen Entsprechung in Form eines Datenfeldes der Art char

Example 4. Copying the structures by means of MQL5
struct str1
{
  double d; // 8 bytes
  long l;   // 8 bytes
  int i[3]; // 3*4=12 bytes
};
struct str2
{
  uchar c[8+8+12]; // str1 structure size
};
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
  str1 src; 
  src.d=-1;
  src.l=20;
  //--- filling the structure parameters
  ArrayInitialize(src.i, 0); 
  str2 dst;
  //--- turning the structure into the byte array
  dst=src; 
}

Auf so unspektakuläre Weise haben wir das Gerüst in ein Datenfeld kopiert.

Um ein etwas „lebendigeres“ Beispiel zu erhalten, betrachten wir die Funktion zur Erstellung einer Programmschnittstelle (Socket).

int connect(SOCKET s, const struct sockaddr *name, int namelen);

In dieser Funktion liegt das Problem in dem zweiten Parameter, der die Speicheradresse des Datengerüstes aufnimmt. Aber wir wissen ja bereits, wie damit umzugehen ist. Also los.

1. Für den Import schreiben wir die Funktion „connect“ in der in MQL5 verfügbaren Form:

int connect(int s, uchar &name[], int namelen);

2. Wir sehen uns die erforderliche Funktion in den „Dokumenten“ an:

struct sockaddr_in
{
  short   sin_family;
  u_short sin_port;
  in_addr sin_addr; // additional 8 byte structure
  char sin_zero[8];
};

3. Wir legen ein Gerüst mit einem Datenfeld vergleichbarer Größe an:

struct ref_sockaddr_in
{
  uchar c[2+2+8+8];
};

4. Nach Ausfüllen des erforderlichen Datengerüstes sockaddr_in übersetzen wir es in ein Byte-Datenfeld, das wir als Parameter an die Importfunktionconnect weitergeben.

Es folgt der anhand dieser Schritte angelegte Programmcode.

Example 5. Referring of the client socket to the server
#import "Ws2_32.dll"
  ushort htons(ushort hostshort);
  ulong inet_addr(char &cp[]);
  int connect(int s, char &name[], int namelen);
#import
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
  //--- connecting the host after the socket initialization

  char ch[];
  StringToCharArray("127.0.0.1", ch);
  //--- preparing the structure
  sockaddr_in addrin;
  addrin.sin_family=AF_INET;
  addrin.sin_addr=inet_addr(ch);
  addrin.sin_port=htons(1000);
  //--- copying the structure to the array
  ref_sockaddr_in ref=addrin; 
  //--- connecting the host
  res=connect(asock, ref.c, sizeof(addrin)); 

  //--- further work with the socket
}

Wie wir sehen, muss für die Arbeit mit Programmstellen keine einzige dynamische Programmbibliothek erstellt werden. Datengerüste können direkt an die Programmierschnittstelle übertragen werden.


3. Arbeiten mit Speicheradressen aus API-Funktionen

Häufig werden von den API-Funktionen die Speicheradressen von Daten in Form von Datengerüsten oder Datenfeldern ausgegeben. Mit den Möglichkeiten von MQL5 können diese Daten nicht extrahiert werden, hier hilft die Funktion memcpy.

Beispiel für die Arbeit mit Speicherdatenfeldern aus einer Speicherplatzzuordnungsdatei (MMF)



Bei der Arbeit mit einer MMF kommt eine Funktion zum Einsatz, die die Speicherplatzadresse zu dem angegebenen Speicherdatenfeld ausgibt.

int MapViewOfFile(int hFile, int DesiredAccess, int OffsetHigh, int OffsetLow, int NumOfBytesToMap);

Das Auslesen der Daten aus diesem Datenfeld erfolgt durch einfaches Kopieren der erforderlichen Anzahl Bytes mithilfe der Funktion memcpy.
Zur Aufzeichnung der Daten in einer Datei wird dieselbe Funktion in ähnlicher Weise eingesetzt.

Example 6. Recording and reading data from MMF memory
#import "kernel32.dll"
  int OpenFileMappingW(int dwDesiredAccess, int bInheritHandle,  string lpName);
  int MapViewOfFile(int hFileMappingObject, int dwDesiredAccess, 
                      int dwFileOffsetHigh, int dwFileOffsetLow, int dwNumberOfBytesToMap);
  int UnmapViewOfFile(int lpBaseAddress);
  int CloseHandle(int hObject);
#import "msvcrt.dll"
  int memcpy(uchar &Destination[], int Source, int Length);
  int memcpy(int Destination, int &Source, int Length);
  int memcpy(int Destination, uchar &Source[], int Length);
#import

#define FILE_MAP_ALL_ACCESS   0x000F001F

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
  //--- opening the memory object
  int hmem=OpenFileMappingW(FILE_MAP_ALL_ACCESS, 0, "Local\\file");
  //--- getting pointer to the memory
  int view=MapViewOfFile(hmem, FILE_MAP_ALL_ACCESS, 0, 0, 0); 
  //--- reading the first 10 bytes from the memory
  uchar src[10];
  memcpy(src, view, 10);
  int num=10;
  //--- recording the 4 byte int number to the memory beginning
  memcpy(view, num, 4);
  //--- unmapping the view
  UnmapViewOfFile(view); 
  //--- closing the object
  CloseHandle(hmem); 
}

Wie wir sehen, gibt es keinerlei Schwierigkeiten bei der Arbeit mit den Speicheradressen in Bezug auf das Speicherdatenfeld. Und die Hauptsache: wir müssen dazu keine zusätzlichen eigenen dynamischen Programmbibliotheken (DLL) anlegen.




Beispiel für die Arbeit mit für MySQL ausgegebenen Datengerüsten

Eine der drängendsten Schwierigkeiten bei der Arbeit mit MySQL bestand darin, Daten aus MySQL zu beziehen. Die Funktion mysql_fetch_row liefert ein Datenfeld mit Zeichenfolgen (Strings). Wobei jede Zeichenfolge wiederum ein Datenfeld aus Feldern ist. Somit gibt diese Funktion die Speicheradressen von Speicheradressen aus. Unsere Aufgabe besteht darin, als diese Daten aus der erhaltenen Speicheradresse zu extrahieren.

Das wird dadurch etwas erschwert, dass es sich bei den Feldern um unterschiedliche Datenarten einschließlich binärer handelt. Das bedeutet, es nicht gelingen wird, sie als Zeichenfolgendatenfeld der Art string wiederzugeben. Zum Abrufen der Angaben zu den Zeichenfolgen und Feldgrößen gibt es die Funktionen mysql_num_rows, mysql_num_fields und mysql_fetch_lengths.

Die Abbildung 4 zeigt den Aufbau der Abbildung des Ergebnisses im Speicher.
Die Adressen des Anfangs dreier Zeichenfolgen sind in einem Datenfeld versammelt. Und die Adresse des Anfangs dieses Datenfeldes (in unserem Beispiel 94) ist das, was die Funktion mysql_fetch_row ausgibt.

Der Aufbau der Abbildung des Abfrageergebnisses im Speicher

Es folgt ein Beispielcode für den Bezug der Daten aus der Datenbankabfrage.

Example 7. Getting data from MySQL
#import "libmysql.dll"
  int mysql_real_query(int mysql, uchar &query[], int length);
  int mysql_store_result(int mysql);
  int mysql_field_count(int mysql);
  uint mysql_num_rows(int result);
  int mysql_num_fields(int result);
  int mysql_fetch_lengths(int result);
  int mysql_fetch_row(int result);
#import 
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
  //--- ... preliminarily initialized mysql data base
  //--- request for getting all the strings from the table
  string query="SELECT * FROM table"; 
  uchar aquery[]; 
  StringToCharArray(query, aquery);

  //--- sending the request
  err=mysql_real_query(mysql, aquery, StringLen(query)); 
  int result=mysql_store_result(mysql);

  //--- in case it contains the strings
  if (result>0) 
  {
    ulong num_rows=mysql_num_rows(result);
    int num_fields=mysql_num_fields(result);    

    //--- getting the first string pointer
    int r=0, row_ptr=mysql_fetch_row(result);
    while(row_ptr>0)
    {

       //--- getting the pointer to the current string columns lengths
      int len_ptr=mysql_fetch_lengths(result); 
      int lens[]; 
       ArrayResize(lens, num_fields);
      //--- getting the sizes of the string fields
      memcpy(lens, len_ptr, num_fields*sizeof(int));
      //--- getting the data fields   
      int field_ptr[];
      ArrayResize(field_ptr, num_fields);
      ArrayInitialize(field_ptr, 0);

      //--- getting the pointers to the fields
      memcpy(field_ptr, row_ptr, num_fields*sizeof(int)); 
      for (int f=0; f<num_fields; f++)
      {
        ArrayResize(byte, lens[f]);
        ArrayInitialize(byte, 0);
         //--- copy the field to the byte array
        if (field_ptr[f]>0 && lens[f]>0) memcpy(byte, field_ptr[f], lens[f]);
      }
      r++;
      //--- getting the pointer to the pointer to the next string
      row_ptr=mysql_fetch_row(result); 
    }
  }
}



4. Auslesen auf NULL endender Zeichenfolgen aus API-Funktionen

Einige API-Funktionen geben die Speicheradresse einer Zeichenfolge aus, ohne uns über die Länge dieser Zeichenfolge zu informieren. In einer solchen Situation haben wir es mit Zeichenfolgen zu tun, die auf Null enden. Anhand dieser Null wird auch das Ende der jeweiligen Zeichenfolge bestimmt. Das bedeutet, dass ihre Länge angegeben werden kann.

Abbildung einer auf NULL endenden Zeichenfolge im Speicher

In der Bibliothek C (msvcrt.dll) ist bereits eine Funktion vorhanden, die den Inhalt auf NULL endender Zeichenfolgen aus diesen in andere Zeichenfolgen kopiert. Dabei bestimmt die die Ausgangszeichenfolge ihre Größe selbst. Es ist es besser, ein Byte-Datenfeld als Empfänger zu verwenden, da die Programmierschnittstelle häufig Zeichenfolgen in Multibyte ausgibt und nicht in Unicode.

strcpy kopiert auf NULL endenden Zeichenfolgen

char *strcpy(char *dst, const char *src);
dst - the pointer to the destination string
src - the pointer to the Null-terminated source string

Eigentlich handelt es sich um einen Sonderfall der Funktion memcpy. Denn das System selbst bricht den Kopiervorgang bei der gefundenen Null in der Zeichenfolge ab. Diese Funktion muss stets bei der Arbeit mit eben diesen Speicheradressen verwendet werden.

In der Programmierschnittstelle von MySQL zum Beispiel gibt es einige Funktionen, die Speicheradressen von Zeichenfolgen ausgeben. Auch der Abruf von Daten aus ihnen ist mithilfe der Funktion strcpy ein Kinderspiel.

Example 8. Getting the strings from the pointers
#import "libmysql.dll"
  int mysql_init(int mysql);
  int mysql_real_connect(int mysql, uchar &host[], uchar &user[], uchar &password[], 
                            uchar &DB[], uint port, uchar &socket[], int clientflag);
  int mysql_get_client_info();
  int mysql_get_host_info(int mysql);
  int mysql_get_server_info(int mysql);
  int mysql_character_set_name(int mysql);
  int mysql_stat(int mysql);
#import "msvcrt.dll"
  int strcpy(uchar &dst[], int src);
#import
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
  uchar byte[];
  ArrayResize(byte, 300);

  int ptr;
  string st;
  //--- pointer to the string
  ptr=mysql_get_client_info();

  if (ptr>0) strcpy(byte, ptr);
  Print("client_info="+CharArrayToString(byte));
  //--- initializing the base
  int mysql=mysql_init(mysql);

  //--- transferring the strings to the byte arrays
  uchar ahost[]; 
  StringToCharArray("localhost", ahost);
  uchar auser[];
  StringToCharArray("root", auser);
  uchar apwd[];
  StringToCharArray("", apwd);
  uchar adb[];
  StringToCharArray("some_db", adb);
  uchar asocket[];
  StringToCharArray("", asocket);
  //--- connecting the base
  int rez=mysql_real_connect(mysql, ahost, auser, apwd, adb, port, asocket, 0);
  //--- determining the connection and the base status
  ptr=mysql_get_host_info(mysql);
  if (ptr>0) strcpy(byte, ptr);
  Print("mysql_host_info="+CharArrayToString(byte));
  ptr=mysql_get_server_info(mysql);
  if (ptr>0) strcpy(byte, ptr);
  Print("mysql_server_info="+CharArrayToString(byte));
  ptr=mysql_character_set_name(mysql);
  if (ptr>0) strcpy(byte, ptr);
  Print("mysql_character_set_name="+CharArrayToString(byte));
  ptr=mysql_stat(mysql);
  if (ptr>0) strcpy(byte, ptr);
  Print("mysql_stat="+CharArrayToString(byte));
}


Fazit

Somit sind durch die Verwendung der drei Grundverfahren für die Arbeit mit Speicherplatz, das Kopieren von Datengerüsten, der Bezug von Speicheradressen und ihren Daten mittels memcpy sowie der Bezug von Zeichenfolgen mit strcpy, imgrunde alle bei der Arbeit mit den unterschiedlichen API-Funktionen anfallenden Aufgaben abgedeckt.

Achtung. Die Arbeit mit den Funktionen memcpy und strcpy ist nicht ganz ungefährlich, wenn dem empfangenden Zwischenspeicher keine ausreichende Datenmenge zugeordnet wird. Achten Sie deshalb aufmerksam auf die Größe der für den Dateneingang ausgesonderten Bereiche.