データの交換方法:10分で書くMQL5のためのDLL

MetaQuotes | 2 10月, 2015

実際、シンプルなDLLライブラリを書く方法や、異なるシステムのバインディング機能は何か正確に覚えている開発者はあまりいないと思います。

いくつか例を挙げながら、10分でDLLを書く手順を網羅するとともに、バインディング実装の技術的詳細もいくらか説明していきたいと思います。ここではビジュアルスタジオ2005/2008を使用します。エクスプレス版はMicrosoftのウェブサイトから無料でダウンロードできます。


1. ビジュアル スタジオ2005/2008でC++言語によるDLLプロジェクト作成

ファイル -> 新規 メニューからWin32アプリケーション ウィザードを実行します。ビジュアルC++を選択、そしてWin32コンソール アプリケーションテンプレートを選択したら、プロジェクト名を付けます。(たとえば、MQL5DLLSamples)プロジェクトロケーションを保存するのに、初期設定で与えられたものの代わりにルートディレクトリを選択します。ソル―ションにディレクトリを作成チェックボックスを無効にしOKをクリックします。

新規DLL プロジェクト

図1 Win32アプリケーション ウィザード:DLLプロジェクト作成

次のステップで次へを押し、設定ページに進みます。

「次の」ページへを選択

図2 Win32アプリケーション ウィザード:プロジェクト設定

最終ページでDLLアプリケーション タイプを選択します。その他のフィールドは入力しません。終了をクリックします。自動で追加された表示コードを削除したくなければ、シンボルのエクスポートオプションは設定しません。

図3 Win32アプリケーション ウィザード:アプリケーション設定

結果得られるのは、空のプロジェクトです。

空のDLL プロジェクト

図4 ウィザードにより準備されたDLLプロジェクト

検証をシンプルにするにはアウトプット ディレクトリ オプションで、DLLファイル ディレクトリのアウトプット をクライアント端末の...\MQL5\Librariesに設定するのがよいでしょう。結果的に時間の節約になります。

DLLアウトプット ディレクトリ

図5 DLLアウトプット ディレクトリ


2. 関数追加準備

stdafx.hファイルの終わりに_DLLAPIマクロを追加します。そうすることで、エクスポートした関数を便利で簡単に記述することができます。

//+------------------------------------------------------------------+
//|                                                 MQL5 DLL Samples |
//|                   Copyright 2001-2010, MetaQuotes Software Corp. |
//|                                        https://www.metaquotes.net |
//+------------------------------------------------------------------+
#pragma once

#include "targetver.h"

#define WIN32_LEAN_AND_MEAN             // Exclude rarely-used stuff from Windows headers
#include <windows.h>

//---
#define _DLLAPI extern "C" __declspec(dllexport)
//+------------------------------------------------------------------+

MQL5で呼ぶDLLがインポートした関数には、stdcallコール規約およびcdeclコール規約があります。 stdcallとcdeclの間でパラメータをスタックから抽出する方法が異なるものの、DLLコールの特別なラッパーによりMQL5のランタイム環境は両者ともで安全に使用されます。

C++コンパイラは色設定では __cdeclコールを使用します。しかし、私はエクスポートされた関数には__stdcallモードを指定することをお薦めします。

正確に書かれたエクスポート関数は以下のフォームをしています。

_DLLAPI int __stdcall fnCalculateSpeed(int &res1,double &res2)
  {
   return(0);
  }

MQL5プログラムでは、関数は以下のように定義されコールされます。

#import "MQL5DLLSamples.dll"
int  fnCalculateSpeed(int &res1,double &res2);
#import

//--- call
   speed=fnCalculateSpeed(res_int,res_double);

プロジェクトがコンパイルされたのち、このstdcallは _fnCalculateSpeed@8としてエクスポート テーブルに表示されます。エクスポート テーブルではコンパイラが下線とバイト数を追加しスタックを通じて変換します。そういったデコレーションにより、コールする者がスタックに配置するデータ数(データタイプではない!)を正確に把握することで、DLLの関数呼び出しの安全性管理がゆきとどくのです。

DLL関数インポート記述のパラメータ ブロックの最終サイズにエラーがあれば、関数はコールされず、ジャーナルに「MQL5DLLSamples.dllにfnCrashTestParametersStdCallが見つかりません。」という新しいメッセージが表示されます。その場合には、関数プロトタイプ、DLLソース両方のパラメータをすべて注意して点検する必要があります。

デコレーションを伴わないシンプルな記述の検索は、エクスポート テーブルが完全な関数名を含まない場合の互換性ために行われます。fnCalculateSpeedのような名前は関数が__cdeclフォーマットに定義されていると作成されます。
_DLLAPI int fnCalculateSpeed(int &res1,double &res2)
  {
   return(0);
  }


3. パラメータを渡しデータを交換するメソッド

渡されたパラメータの変数について考察します。

  1. シンプルな変数の受け取りと受け渡し
    シンプルな変数の場合は簡単です。値または&を使った参照により渡されます。
    _DLLAPI int __stdcall fnCalculateSpeed(int &res1,double &res2)
      {
       int    res_int=0;
       double res_double=0.0;
       int    start=GetTickCount();
    //--- simple math calculations
       for(int i=0;i<=10000000;i++)
         {
          res_int+=i*i;
          res_int++;
          res_double+=i*i;
          res_double++;
         }
    //--- set calculation results
       res1=res_int;
       res2=res_double;
    //--- return calculation time 
       return(GetTickCount()-start);
      }
    MQL5からのコール
    #import "MQL5DLLSamples.dll"
    int  fnCalculateSpeed(int &res1,double &res2);
    #import
    
    //--- calling the function for calculations
       int    speed=0;
       int    res_int=0;
       double res_double=0.0;
    
       speed=fnCalculateSpeed(res_int,res_double);
       Print("Time ",speed," msec, int: ",res_int," double: ",res_double);
    アウトプットは
    MQL5DLL Test (GBPUSD,M1) 19:56:42 Time  16  msec, int:  -752584127  double:  17247836076609
  2. エレメント記述による配列の受け渡し

    他のMQL5プログラムとは異なり、配列渡しはディメンションやサイズの所有元情報にアクセスすることなく、直接データ バッファへの参照を通じておこなわれます。 配列のディメンションやサイズが別途渡されるのはこのためです。

    _DLLAPI void __stdcall fnFillArray(int *arr,const int arr_size)
      {
    //--- check for the input parameters
       if(arr==NULL || arr_size<1) return;
    //--- fill array with values
       for(int i=0;i<arr_size;i++) arr[i]=i;
      }
    MQL5からのコール
    #import "MQL5DLLSamples.dll"
    void fnFillArray(int &arr[],int arr_size);
    #import
    
    //--- call for the array filling
       int    arr[];
       string result="Array: "; 
       ArrayResize(arr,10);
       
       fnFillArray(arr,ArraySize(arr));
       for(int i=0;i<ArraySize(arr);i++) result=result+IntegerToString(arr[i])+" ";
       Print(result);
    アウトプットは
    MQL5DLL Test (GBPUSD,M1) 20:31:12 Array: 0 1 2 3 4 5 6 7 8 9
  3. ストリングの受け渡しと修正
    ユニコード ストリングは追加情報を渡すことなく、そのバッファ アドレスを直接参照して渡されます。
    _DLLAPI void fnReplaceString(wchar_t *text,wchar_t *from,wchar_t *to)
      {
       wchar_t *cp;
    //--- parameters check
       if(text==NULL || from==NULL || to==NULL) return;
       if(wcslen(from)!=wcslen(to))             return;
    //--- search for substring
       if((cp=wcsstr(text,from))==NULL)         return;
    //--- replace it
       memcpy(cp,to,wcslen(to)*sizeof(wchar_t));
      }
    MQL5からのコール
    #import "MQL5DLLSamples.dll"
    void fnReplaceString(string text,string from,string to);
    #import
    
    //--- modify the string
       string text="A quick brown fox jumps over the lazy dog"; 
       
       fnReplaceString(text,"fox","cat");
       Print("Replace: ",text);
    結果は
    MQL5DLL Test (GBPUSD,M1) 19:56:42 Replace:  A quick brown fox jumps over the lazy dog
    行が変わっていないことがわかりますね!これは経験の浅い人が、参照する代わりにオブジェクト(オブジェクトのストリング)のコピーを送信するときに犯しやすい誤りです。DLLで修正されたストリング'text'のコピー は自動生成され、その後元のストリングに影響を与えることなく自動で削除されます。

    この状況を改善するためには、ストリングを参照によって渡す必要があります。そのためには、"text"パラメータに&を追加することでインポートのブロックを単純に修正します。
    #import "MQL5DLLSamples.dll"
    void fnReplaceString(string &text,string from,string to);
    #import
    コンパイルし、開始したら正しい結果が得られます。
    MQL5DLL Test (GBPUSD,M1) 19:58:31 Replace:  A quick brown cat jumps over the lazy dog


4. DLL関数の例外を取得

端末のクラッシュを避けるため、DLL呼び出しは個別に未処理例外ラッピングで自動的に保護されています。このメカニズムにより、標準的なエラー(メモリアクセスエラーゼロ除算など)から保護することができます。

このメカニズムがどのように働くか見るため、次のコードを作成してみます。

_DLLAPI void __stdcall fnCrashTest(int *arr)
  {
//--- wait for receipt of a zero reference to call the exception
   *arr=0;
  }

そして、クライアント端末から呼びます。

#import "MQL5DLLSamples.dll"
void fnCrashTest(int arr);
#import

//--- call for the crash (the execution environment will catch the exception and prevent the client terminal crush)
   fnCrashTest(NULL);
   Print("You won't see this text!");
//---

結果、ゼロ アドレスに書き込もうとし例外を引き起こします。クライアント端末はそれを察知し、ジャーナルにログを残し、作業を続けます。

MQL5DLL Test (GBPUSD,M1) 20:31:12 Access violation write to 0x00000000


5. DLLはラッパーを呼びコールのスピードを失速します

上記で述べたとおり、DLL関数のコールは安全性確保のため、特別なラッパーでラップされています。このバインディングは基本コードにマスクをかけ、スタックを置き換え、stdcall/cdecl同意をサポートし、呼ばれている関数内の例外を監視します。

この作業ボリュームは関数呼び出しにたいした遅れをもたらすことはありません。


6. 最終構築

MQL5DLLSamples.cppファイルに上記のDLL関数例を、またスクリプトMQL5DLL Test.mq5にMQL5例を集積します。ビジュアル スタジオ2008MQL5内のスクリプトに対する最終プロジェクトは本稿に添付しています。

//+------------------------------------------------------------------+
//|                                                 MQL5 DLL Samples |
//|                   Copyright 2001-2010, MetaQuotes Software Corp. |
//|                                        https://www.metaquotes.net |
//+------------------------------------------------------------------+
#include "stdafx.h"

//+------------------------------------------------------------------+
//| Passing and receving of simple variables                         |
//+------------------------------------------------------------------+
_DLLAPI int __stdcall fnCalculateSpeed(int &res1,double &res2)
  {
   int    res_int=0;
   double res_double=0.0;
   int    start=GetTickCount();
//--- simple math calculations
   for(int i=0;i<=10000000;i++)
     {
      res_int+=i*i;
      res_int++;
      res_double+=i*i;
      res_double++;
     }
//--- set calculation results
   res1=res_int;
   res2=res_double;
//--- return calculation time
   return(GetTickCount()-start);
  }
//+------------------------------------------------------------------+
//| Filling the array with values                                    |
//+------------------------------------------------------------------+
_DLLAPI void __stdcall fnFillArray(int *arr,const int arr_size)
  {
//--- check input variables
   if(arr==NULL || arr_size<1) return;
//--- fill array with values
   for(int i=0;i<arr_size;i++) arr[i]=i;
  }
//+------------------------------------------------------------------+
//| The substring replacement of the text string                     |
//| the string is passed as direct reference to the string content   |
//+------------------------------------------------------------------+
_DLLAPI void fnReplaceString(wchar_t *text,wchar_t *from,wchar_t *to)
  {
   wchar_t *cp;
//--- parameters checking
   if(text==NULL || from==NULL || to==NULL) return;
   if(wcslen(from)!=wcslen(to))             return;
//--- search for substring
   if((cp=wcsstr(text,from))==NULL)         return;
//--- replace it 
   memcpy(cp,to,wcslen(to)*sizeof(wchar_t));
  }
//+------------------------------------------------------------------+
//| Call for the crush                                               |
//+------------------------------------------------------------------+
_DLLAPI void __stdcall fnCrashTest(int *arr)
  {
//--- wait for receipt of a zero reference to call the exception
   *arr=0;
  }
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//|                                                 MQL5DLL Test.mq5 |
//|                        Copyright 2010, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "2010, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
//---
#import "MQL5DLLSamples.dll"
int  fnCalculateSpeed(int &res1,double &res2);
void fnFillArray(int &arr[],int arr_size);
void fnReplaceString(string text,string from,string to);
void fnCrashTest(int arr);
#import

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- calling the function for calculations
   int    speed=0;
   int    res_int=0;
   double res_double=0.0;

   speed=fnCalculateSpeed(res_int,res_double);
   Print("Time ",speed," msec, int: ",res_int," double: ",res_double);
//--- call for the array filling
   int    arr[];
   string result="Array: "; 
   ArrayResize(arr,10);
   
   fnFillArray(arr,ArraySize(arr));
   for(int i=0;i<ArraySize(arr);i++) result=result+IntegerToString(arr[i])+" ";
   Print(result);
//--- modifying the string
   string text="A quick brown fox jumps over the lazy dog"; 
   
   fnReplaceString(text,"fox","cat");
   Print("Replace: ",text);
//--- and finally call a crash
//--- (the execution environment will catch the exception and prevent the client terminal crush)
   fnCrashTest(NULL);
   Print("You won't see this text!");
//---
  }
//+------------------------------------------------------------------+

今回の内容に関心をお持ちいただきありがとうございました!ご質問はなんなりとお寄せください。