Monday, May 12, 2014

Code to Export C# DLL to Metatrader Build 600+

This is a quick followup to the article Code to Export C# DLL to Metatrader to update the code for Metatrader build 600+ due to the many comments I have received.

The good news: most things stay the same, particularly with regards to the dll call from MQL4. The changes in C# are relatively straightforward.

With build 600+, strings must now be explicitly specified in C# with the following format:
[return: MarshalAs(UnmanagedType.LPWStr)]

I added in this update two array examples. The first shows how to pass a double array as a parameter to C# and do something with those values (calculate the mean of 5 sample values) in C#. The second example shows how to pass an array by reference to C#. The MT4 end of things is identical. In C# you simply specify [In, Out...] to allow the array to be bi-directional.

Here is the C# (updated) code:
using System;
using System.Text;
using RGiesecke.DllExport;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace testUMD600
{
   class Test
   {
 
        [DllExport("AddInteger", CallingConvention = CallingConvention.StdCall)]
        public static int AddInteger(int Value1, int Value2) {
            MessageBox.Show("Add Integers: " + Value1.ToString() + " " + Value2.ToString(), "AddInteger");
            return (Value1 + Value2);
        }

        [DllExport("AddDouble", CallingConvention = CallingConvention.StdCall)]
        public static double AddDouble(double Value1, double Value2) {
            MessageBox.Show("AddDouble: " + Value1.ToString() + " " + Value2.ToString(), "AddDouble");
            double Value3 = Value1 + Value2;
            return (Value3);
        }

        [DllExport("AddDoubleString", CallingConvention = CallingConvention.StdCall)]
        [return: MarshalAs(UnmanagedType.LPWStr)]  // note this change for build 600+
        public static string AddDoubleString(double  Value1, double Value2) {
            MessageBox.Show("AddDoubleString: " + Value1.ToString() + " " + Value2.ToString(), "AddDoubleString");
            double Value3 = Value1 + Value2;
            return (Value3.ToString() );
        }

        [DllExport("returnString", CallingConvention = CallingConvention.StdCall)]
        [return: MarshalAs(UnmanagedType.LPWStr)] // note this change for build 600+ as well as MarshalAs statement on the next line...
        public static string returnString ([MarshalAs(UnmanagedType.LPWStr)] string Input) {
            MessageBox.Show("Received: " + Input, "returnString");
            return ("SEND to MT4");
        }

        // many thanks to anonymous for the code sample below!
        [DllExport("ReturnDouble2", CallingConvention = System.Runtime.InteropServices.CallingConvention.StdCall)]
        static double ReturnDouble2() {
            return 4.5;
        }

       // a double array is passed in as a parameter from MT4, for the purposes of this demo, 
       // only the first five (or fewer) items in array data are processed.
       // An average is created from the first 5 items in data and returned to MT4, 
       // and those first 5 items items are shown in a message box for validation purposes.
        [DllExport("PassDoubleArray", CallingConvention = CallingConvention.StdCall)]
        public static double PassDoubleArray ([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] double[] data,
            int datasize)  
        {
            string ff = "first five items (or less):";
            int end = 5;
            if (end > data.Length) end = data.Length;
            double ave = 0.0;
            for (int i = 0; i < end; i++) {
                ff = ff + " " + data[i].ToString();
                ave += data[i];
            }
            if (end == 0) end = 1;
            ave /= end;
            MessageBox.Show("Received " + ff, "PassDoubleArray");
            return (ave);
        }

       /*
       NOTE:

        The key with array passing (as a parameter to C#) is the SizeParamIndex field (above).
        When SizeParamIndex is equal to 1 (shown above) the size of the array data must be contained
        In the parameter in slot 1 (the 2nd parameter). 
        This is fulfilled by the variable datasize. If datasize were the first parameter, then SizeParamIndex = 0 would
        be appropriate.

       This is inferred from this page:
       www.mql5.com/en/articles/249
       (See 4.3 example 3 as well as some others) 
       Note how both arrays reference SizeParamIndex = 1 which is the "len" parameter in slot 1 (2nd parameter).
       
       Also note the use of [In Out MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] when passing an array by reference in that example,
       and below in PassDoubleArrayByref example.
       */

        [DllExport("PassDoubleArrayByref", CallingConvention = CallingConvention.StdCall)]
        public static double PassDoubleArrayByref ([In, Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] double[] data,
            int datasize) {
            string ff = "first five items (or less):";
            int end = 5;
            if (end > data.Length) end = data.Length;
            double mean = 0.0;
            for (int i = 0; i < end; i++) {
                ff = ff + " " + data[i].ToString();
                mean += data[i];
            }
            
            if (end == 0) end = 1;
            mean /= end;
            // subtract the mean from each of the first values in data
            ff = ff + "\r\n" + "Data passed back to MT4: " + "\r\n" ;
            for (int i = 0; i < end; i++) {
                data[i] -= mean; // subtract the mean
                ff = ff + " " + data[i].ToString();
            }
            MessageBox.Show("Received " + ff, "PassDoubleArrayByref");
            return (mean);
        }

        [DllExport("PassStringArrayByref", CallingConvention = CallingConvention.StdCall)]
        public static void PassStringArrayByref ([In, Out, MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPStr, SizeParamIndex = 1)] IntPtr[] stringPointer, int datasize) {
            try {
                // I don't know why we need to pass a fake size 3x larger than the array size but this seems to work this way.
                // Use at your own risk!!!
                // Not every index contains string values so you must go through and select the items with a valid pointer
                string[] data = new string[stringPointer.Length/3];
                int datacount = -1; // used to properly index the data array
                for (int i = 0; i < stringPointer.Length; i++) {
                    if (stringPointer[i] != IntPtr.Zero) {
                        datacount++;
                        data[datacount] = Marshal.PtrToStringAuto(stringPointer[i]);
                        if (datacount == 0) {
                            // attempt to alter the incoming string going back to MT4
                            stringPointer[i] = Marshal.StringToBSTR("String returned byref from C# DLL!!!");
                        } else {
                            // show how it is possible to append data to an existing string (symbol names) and send back to MT4
                            System.Security.SecureString alteredData = new System.Security.SecureString();
                            foreach (char c in (data[datacount] + "_modified by C#")) {
                                alteredData.AppendChar(c);
                            }
                            stringPointer[i] = Marshal.SecureStringToBSTR(alteredData);
                        }
                        MessageBox.Show(data[datacount], "PassStringArrayByref");
                    } else {
                        MessageBox.Show("Pointer " + i.ToString() + " is zero!", "PassStringArrayByref");
                    }
                }
            } catch (Exception ex) {
                MessageBox.Show("Exception at PassStringArrayByref: " + ex.ToString(), "PassStringArrayByref");
            }
        }


   }
}



Here is the MT4 (updated) code:
//+------------------------------------------------------------------+
//|                                                   testDLL600.mq4 |
//|                               Copyright © 2014, Patrick M. White |
//|                     https://sites.google.com/site/marketformula/ |
//|                                    updated 5/12/2014             |
//+------------------------------------------------------------------+
#property copyright "Copyright © 2014, Patrick M. White"
#property link      "https://sites.google.com/site/marketformula/"
#property version   "1.00"
#property strict

#import "testUMD600.dll"
   // nothing changed on the MT4 side. String handling changed in C#
   int AddInteger(int Value1, int Value2);
   double AddDouble(double Value1, double Value2);
   string AddDoubleString(double Value1, double Value2);
   string returnString(string Input);
   double ReturnDouble2(); 
   
   // note that the two calls below are identical, the key is in the C# dll adding 
   // [In, Out... to PassDoubleArrayByref
   double PassDoubleArray(double &data[], int datasize);
   double PassDoubleArrayByref(double &data[], int datasize); 
   void PassStringArrayByref(string &data[], int datasize); 
#import
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   Print("AddInteger: " + DoubleToStr(AddInteger(250, 750),0));
   double a = AddDouble(250,750);
   Print("AddDouble: " + DoubleToStr(a,4));

   double d = StrToDouble(AddDoubleString(250, 750));
   Print("AddDoubleString: " + DoubleToStr(d,4));
   string temp = "Send to DLL";
   string recv = returnString(temp);
   Print(recv);
   double dd = ReturnDouble2();
   Print("Returning Double from C#: " + DoubleToStr(dd, 4));
   
   double data[];
   ArrayResize(data, 5);
   string s = "Sending: ";
   for (int i = 0; i < ArraySize(data); i++) {
      data[i] = i + 1.0;
      s = s + " " + DoubleToStr(data[i],1);
   }
   Print(s);
   double ret = PassDoubleArray(data, ArraySize(data));
   Print("Mean of 1+2+3+4+5: " + DoubleToStr(ret, 4));
   
   ret = PassDoubleArrayByref(data, ArraySize(data));
   s = "Receiving sent values less their mean: ";
   for (int i = 0; i < ArraySize(data); i++) {
      s = s + " " + DoubleToStr(data[i],2);
   }
   Print(s);
   
   // string array demo:
   string sa[];
   ArrayResize(sa, 4);
   sa[0] = "Test from MT4 to DLL";
   sa[1] = "EURUSD";
   sa[2] = "USDJPY";
   sa[3] = "AUDUSD";
   PassStringArrayByref(sa, ArraySize(sa)*3); // pass fake size 3x larger - why? it works! 
   for (int i = 0; i < ArraySize(sa); i++) {
      Print("PassStringArrayByref index: " + i + " size: " + ArraySize(sa) + " text: " +  sa[i]);
   }

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



I just tested the above script in MT4 build 646 and it worked correctly.

As a reminder, compile the dll in C# with Platform / Platform Target for x86 (Project / Properties / Build ). 

The build 600+ project can be downloaded at Code to Export CSharp DLL to Metatrader 600+.

Instructions to get the Unmanaged Exports (DllExport for .NET) for C# via nuget can be found here.

Edit and VERY important note: you MUST handle all errors produced by your DLL, inside your DLL or those errors will feed back to the calling application, causing MT4 to shut down. While the code samples above notably don't show this implemented, you should include a default try{} catch (Exception ex) {} block in each of your C# methods to prevent this from happening. There have been MANY comments from those attempting to implement this code that has prompted this edit, so take note!

6/8/2015 Edit: Added code to allow byref passing of string arrays to/from C# and a demo on how one might use this to pass symbol names to the DLL, modify a string in the DLL and pass back to MT4.  Note only the code above in this blog post has been changed. The uploaded code has not been updated, so copy/paste here to get the updated code. Also note that this was done by trial and error and so the 3x array size hack may not work in all cases. So do your own testing and report back what you learn. If you have a better way to define or make the method call  for passing a string array from MT4 to C#, please report your findings here so we can all learn.