自作 DLL の排除

--- | 28 10月, 2015


まだご自身で DLL を書いていますか?
本稿はそんな方向けです!


はじめに

MQL5 言語機能がタスク遂行に十分ではない瞬間はなんどきでも訪れます。そのような場合、MQL5 プログラマーは別のツールを使用する必要があります。たとえば、データベースと連携する、コミュニケーションソケットを使用する、オペレーションシステムの関数を利用するなどが可能です。MQL5 プログラマーはまた各人が使用するMQL5 の機能を拡げるため多様な API を扱う必要があります。ただ、いくつかの理由によりプログラマーは MQL5から直接必要な関数にアクセスすることができません。それはプログラマーに以下の知識がないからです。

よってプログラマーは別のプログラム言語を使わざるを得ず、そのため必要な機能性と連携するために仲介 DLL を作成することとなるのです。MQL5 に多様なデータタイプを表現しそれらを APIに転送する機能があったとしても、残念ながら MQL5 は受け付けられたポインタからデータを抽出することに関する問題を解決することはできません。

本稿ではすべての "i" にドットを打ち、複雑なデータタイプを転送し受け取り、返されたインデックスに連携するメカニズムを示していきます。


コンテンツ

1. メモリがすべて

2. ストラクチャの API 関数への転送

3. API 関数ポインタとの連携

4. API 関数からのNULL 終端ストリング読み出し



1. メモリがすべて

ご存知かと思いますが、あらゆる変数(複雑なデータタイプ変数を含む)には特定のアドレスがあり、そこからその変数はメモリに格納されます。このアドレスは 4 バイトの整数値(int タイプの)で、この変数の最初のバイトアドレスに等しくなっています。

すべてがうまく定義されると、それはこのメモリ領域と連携することができます。C 言語ライブラリ(msvcrt.dll)には memcpy 関数が含まれます。その目的要素がないことに気づくことです。それは MQL5 と多様な API ライブラリをバインドし、プログラマーに大きな可能性を提供します。


先人の知識を参照しよう

Memcpy 関数はあるバッファから別のバッファに指定のバイト数をコピーし、レシーババッファにポインタを返します。

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

すなわち、src アドレスから始まるcnt バイトサイズのメモリ領域が dst アドレスから始まるメモリ領域にコピーされるのです。

src アドレスにあるデータタイプは多様です。char タイプの1バイト変数、double タイプの8バイトの数、配列、あらゆるストラクチャ、メモリボリュームなどです。そのことはアドレスとサイズがわかっていれば、データをある領域から別の領域に自由に送信できることを意味しています。


どのように動作するか

ダイアグラム 1 はいくつかのデータタイプの比較サイズを示しています。

MQL5 における多様なデータタイプのサイズ


Memcpy 関数はあるメモリ領域から別のメモリ領域にデータコピーをするのに必要です。
図2は4バイトデータのコピーを示しています。

memcpy を使用した4バイトデータのコピー例

MQL5 では以下のように表記されます。

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

多様なデータタイプ(同じcntサイズ)が指定のメモリ領域 dst およびsrcとして使用可能であることに注意が必要です。たとえば src ポインタは double 変数(cnt=8 バイト)を参照することができ、 dst はサイズ char[8] または int[2]の配列を参照可能です。

そのときプログラマーがメモリに対してどのような考え方をしているかは問題ではありません。配列 char[8] か、または単にひとつの長い変数か、はたまたストラクチャ { int a1; int a2; }であるかは問いません。

メモリデータは多様なタイプのデータとみなすことができますたとえば5バイトの配列を {int i; char c;} ストラクチャに転送したり、その逆も可能です。この関係により API 関数と直接連動するチャンスを得るのです。

確定順で memcpy アプリケーションバージョンを検証します。


インデックスの取得

例1でmemcpy 関数が dst 変数アドレスを返すことを示しました。

このプロパティはあらゆる変数(その他の複雑タイプ配列を含み)を取得するのに使用可能です。そのためには、同じ変数をソースパラメータおよびレシーバパラメータとして指定するだけです。cnt では 0 を転送することが可能です。というのも実際のコピーは必要ないからです。

たとえば double 変数と short 配列のアドレスを取得します。

2 変数に対するポインター取得
#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); 
}

受け取られたアドレスは必要な API 関数に転送することができます。またはストラクチャパラメータや同じ memcpy 関数のパラメータとしても転送することができます。


配列のコピー

ご存じのように配列は専用メモリのかたまりです。専用メモリのサイズはエレメントタイプとその量に依存します。たとえば短い配列タイプでエレメント数が10であれば、 そのような配列はメモリに 20 バイトをコピーします(ショート サイズは 2 バイトです)。

ただこれら 20 バイトは 20 の char タイプデータまたは 5 のintタイプデータで構成される配列として示されています。いずれにせよ、それらはメモリ内で 20 バイトを取ります。

配列をコピーするには以下を行う必要があります。

3 配列のコピー
#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. ストラクチャの API 関数への転送

書かれたストラクチャポインタを API に転送する必要があるとします。MQL5 言語はストラクチャ送信の限度を設定します。本稿の冒頭でメモリは別に存在することができると述べました。それは必要なストラクチャが MQL5 によってサポートされるデータタイプにコピーされることを意味しています。通常配列はストラクチャに適したタイプとなっています。そのためストラクチャから配列を取得し、配列を API 関数に転送するのです。

ストラクチャを使用してメモリをコピーするオプションは ドキュメンテーション項で述べます。ここではmemcpy 関数を使用することはできません。というのもストラクチャをパラメータとして転送することが不可能で、ストラクチャのコピーがここでは唯一の方法だからです。

図3は異なるタイプの5個の変数で構成されるストラクチャの表現とchar 配列として示されるそれと等価のものを表示しています。

異なるタイプの5個の変数で構成されるストラクチャと char 配列としてのその等価物の提示

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

そのようなシンプルな方法でストラクチャをバイトデータにコピーしました。

この例をより実用的なものとするためソケット作成関数を考察します。

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

この関数では二番目のパラメータが問題です。ストラクチャに対するポインタを受け入れてしまうからです。ですがそれにどう対処するかすでにわかっています。というわけで始めましょう。

1. MQL5で許容されるメソッドを用いてインポートのためのconnect 関数を書きます。

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

2. ドキュメンテーションで必要なストラクチャをよく見ます。

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

3. 同じようなサイズの配列を持つストラクチャを作成します。

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

4. 必要な sockaddr_in ストラクチャを書いたら、それをバイト 配列に転送し、connect パラメータとしてサブミットします。

以下は前述の手順に従って作成したコードセクションです。

5 サーバーへのクライアントソケット指示
#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
}

ご覧のようにご自身の DLL を作成する必要はまったくありません。ストラクチャは API に直接転送されます。


3. API 関数ポインタとの連携

ほとんどの場合、API 関数はデータにポインタを返します。それはストラクチャおよび配列です。MQL5 はデータ抽出には適していません。よってここでは memcpy 関数を使用します。

「メモリマッピングファイル(MMF)」からのメモリ配列との連携例



MMF と連携する場合、その関数が使用されます。それは専用メモリ配列にポインタを返します。

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

この配列からのデータ 読み出しmemcpy関数によって必要なバイト量をコピーするだけで実行されます。
配列にデータを書き込むのも memcpy を用いて行います。

6 レコードと MMF メモリからのデータ読み出し
#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); 
}

見てのとおりメモリ配列用のポインタと連携するのはそれほど難しいことではありません。もっと重要なことは、そのためにご自身でそれ以外の DLL を作成する必要がないということです。




MySQL に返されるストラクチャとの連携例

MySQL と連携する場合の緊急課題の一つはそこからのデータ取得です。mysql_fetch_row 関数はストリング配列を返します。各ストリングはフィールド配列です。よってこの関数はポインタにポインタを返します。われわれのタスクは返されたポインタからこれらデータをすべて抽出することです。

フィールドがバイナリデータを含む多様なデータタイプであることでタスクはやや込み入っています。そういったデータをストリング配列として示すことはできないということです。関数 mysql_num_rows、mysql_num_fields、 mysql_fetch_lengths はストリングおよびフィールドサイズに関する情報を取得するのに使用されます。

図4はメモリ内に結果を示すトラクチャを表示しています。
これらストリングの冒頭のアドレスは配列に集められます。そして配列冒頭のアドレス(例では= 94)は mysql_fetch_row 関数が返すものです。

メモリ内でリクエスト結果を示すストラクチャ

次はデータベースリクエストから取得するデータに対するコード例です。

7 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. API 関数からのNULL 終端ストリング読み出し

API 関数の中にはストリングにポインタを返すものもありますが、このストリングの長さは表示しません。ここでは、ゼロで終わるストリングを論じます。このゼロはストリングの終了部を判断するのに役立ちます。これはストリングサイズが指定可能であるということです。

メモリ内での NULL終端ストリングの表示

C (msvcrt.dll) 言語ライブラリはすでに適切なポインタから別のストリングに NULL終端ストリングのコンテンツをコピーする関数を備えています。ストリングサイズは関数によって決定されます。レシーバとしてバイト配列を使用することが望まれます。 API はUnicodeではなくマルチバイトストリングを返すことがよくあるからです。

strcpy -NULL で終わっているストリングをコピーします。

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

実際、それは memcpy 関数の特殊な例です。ストリングにゼロを見つけるとシステムはコピーを停止します。この関数はつねにそのようなポインタと連携するのに使用されます。

たとえばストリングにポインタを返す MySQL からの API には複数の関数があります。そして strcpy を使用してそれらからデータを取得するのは些末なタスクです。

8 ポインターからのストリング取得
#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));
}


おわりに

これまで述べてきたように、多様な API 関数と連携する場合、メモリとの連携(ストラクチャのコピー、memcpyにおけるポインタおよびそのデータ取得、strcpyストリング取得)に関する3つの基本メカニズムを使用することで実質的にすべてのタスクに応じるのです。

警告レシーババッファに十分なデータ量が割り当てられていない限り memcpy および strcpy と連携するのは安全ではありません。よって受け取られるデータに対する割当てサイズには注意が必要です。