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.

27 comments:

Ken OKabe said...

Thank you very much, You helped me a lot!

Patrick White said...

Ken - Glad you found the info useful!

Anonymous said...

Thanks a lot for the code!
Also happy to report that it also works on MT5 build 975, Win7 64, VS2012.
Just need to change the target platform to x64 :)

Patrick White said...

You're welcome. It's good to hear that the code works with MT5! Thanks for testing the code and including your detailed stats, as well as the hint about 64 bit DLL compilation for compatibility with MT5!

masterpae said...

Dear Patrick,
Thank you very much for this interesting ideea. I have tryed to compile your solution on Visual Studio 2013 Ultimate. Sadly i received this error:
"1>------ Build started: Project: testUnmanagedDLL, Configuration: Debug x86 ------
1> testUnmanagedDLL -> A:\[C_NET_Projects]\testUnmanagedDLL\testUnmanagedDLL\bin\Debug\x86\testUMD600.dll
1> ILDasm: calling 'ildasm.exe' with /quoteallnames /unicode /nobar /linenum "/out:C:\Users\adi\AppData\Local\Temp\tmp6DE3\testUMD600.il" "A:\[C_NET_Projects]\testUnmanagedDLL\testUnmanagedDLL\bin\Debug\x86\testUMD600.dll"
1>A:\[C_NET_Projects]\testUnmanagedDLL\testUnmanagedDLL\DllExport\RGiesecke.DllExport.targets(8,5): error : The system cannot find the file specified
========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped =========="
I am a beginer in C#, and I was unable to understand what is hapening. Can you please help me out compile your exemple?
Should I use another VS? 2012 or 2010? What .NET version should i target? 4? 4.5?
If in the end i will be able to obtain the dll, what is the highes MT4 version I should use? Can it run on the last MT4 version 735?
Here is my email if needed: adrianpreoteasa@yahoo.com
Thank you.

Patrick White said...

masterpae,

I'm not sure exactly. You may have to do some searching to find what the error is. Can you compile any project? Here is one link I found that might help. Did you install Robert Giesecke's project?

http://stackoverflow.com/questions/18719094/vs-express-2013-rc-for-web-system-cannot-find-the-file-specified-error

Unknown said...

Hi, thanks for your hard work!!
How can I debug the C# code.
I've tried to "Attach to process" but no breakpoint was hit.

Patrick White said...

Hi Andrea,

I've had to go back to old school methods to determine which function was active at the time. I've used message boxes for this but it won't work in all cases and isn't ideal. But it may help you determine which block of code causes an error.

Better would be creating a debug file with your dll and logging (appending) text each step of the functions you wish to debug. When the text stops you have narrowed down where the problem lies and can refine the debugging from there. As far as being able to see variable values and that - you may just have to print those values to file to see what result your code is actually producing at a given point. Hope that helps!

sparky said...

Patrick, your project cannot be found at that google site. You have an alternative link ?

Patrick White said...

sparky,
There is no alternative download site for the project, but the entire code is in this post's body, so simply copy, paste and compile.

Unknown said...

i have a problem like masterpae

">C:\Users\daigv\Documents\Visual Studio 2013\Projects\testUnmanagedDLL600\testUnmanagedDLL\testUnmanagedDLL\DllExport\RGiesecke.DllExport.targets(8,5): error : The system cannot find the file specified"

I hope u can help me!

Thank's you!

P/S : i using windows 10 techpre, VS 2013 , .net 4.5

Patrick White said...

Hi Dai,

To get a more detailed error message when you build by changing the setting in VS2013: (Menus)

Tools/Options/Projects and Solutions/ Build and Run/ MSBuild project build output verbosity from minimal to Diagnostic.

EDIT: I just had a look at VS2013 with this project. Basically under References, it is missing Giesecke's project. Go Project/Manage NuGet packages, then search for 'Giesecke' and install it. It now compiles without error. Also change target build under Project/Properties/Build platform target to x86 instead of any CPU for MT4 compatibility. Good Luck!

Jonny said...

thank u very much.

i trying send a string[] C# to string[] MT4 but no ideal.

i hope u can help me!

Patrick White said...

Hi Jonny,

Can you be more specific about what you've tried and what the result was? Have you tried using the array format found in the two sample calls: PassDoubleArray and PassDoubleArrayByref

Those calls show how to pass double[]. Strings are tricky in that they need to be marshalled: see returnString for how to do the marshalling of strings. You may need to combine these two concepts to get it working.

I'm not sure why I would ever need to pass in a string[] or return a string[], however. Why not use the primitive types (double/int) for market data rather than dealing with strings at all?

Unknown said...

Hi Patrick,

Thanks for sharing.
I've tried via the VB.Net route but did not have any luck with the code and got the following error;

Cannot load 'MQL4_DLL.dll' [126]

Appreciate if you could advise on this. Thanks.

Patrick White said...

Hi James,

Metatrader isn't finding the (file) dll. Error 126 in MQL4 is The specified module could not be found. ERROR_MOD_NOT_FOUND. Check to be sure your dll is in the directory MT4 expects it to be located: ...\MQL4\Libraries\ and make sure your startup shortcut for MT4 has the /portable flag at the end so it will look in that directory, such as:

"...\MetaTrader\terminal.exe" /portable

Also check if you have specified a specific path with your dll name that this path is correct / where you saved the dll.

If you make it past error 126, you may get "lucky" enough to trigger error 127 if you didn't set your dll to build as a 32 bit app.

Antony said...

Hi Patrick,
thank you for your example. Unfortunately it doesn't work for me.
I receive the following error:
2015.06.06 21:48:45.913 unresolved import function call
2015.06.06 21:48:45.913 Cannot find 'AddInteger' in 'testUMD600.dll'

I just try to test one function in a MT4 Script from your example.
What I did:
Migrated your solution to VS2013.
RGiesecke.DllExport.Metadata is referenced correctly and I can build x86 dll without any problems:
..2013\Projects\testUnmanagedDLL\testUnmanagedDLL\bin\Release\x86\testUMD600.dll
The created dll is located in the \library folder of MQL4.
Allow DLL imports is checked.
The Version of my MT4 is 830.
Do you have any idea why it is not working?
Your help is greatly appreciated.
Thank you,
Antony

Patrick White said...

Hi Antony,

This error message seems to say that MT4 has found your dll in the location you saved/referenced it, but is unable to find the function call "AddInteger" within your dll.

Double check the case of your calls: AddInteger must be spelled exactly the same / with same case in both the dll and in the MQL4 code. If everything else was done correctly as you say, then this is probably the error.

The usual reason for this error, assuming you were using the code provided is that your dll is not compiled as x86. Please make sure that "any cpu" is not selected, and try recompiling again. Note the exact output path shown on the output window in VS 2013, and copy the dll from there. The next reason is that the function wasn't properly exported. So check/double check that your export code is the same as is listed above, i.e.:

[DllExport("AddInteger", CallingConvention = CallingConvention.StdCall)]

Next make sure that the reference to Giesecke's tool is in your project. It probably is or the VS2013 project wouldn't compile. If it isn't that, I'm not sure what it could be.

Eddy Lai said...

I tried to add NetMQ into the sample DLL (want to call netMQ later),
--------------------------------------------
NetMQContext ctx = NetMQContext.Create();
NetMQ.Sockets.RequestSocket client = ctx.CreateRequestSocket();
client.Connect("tcp://127.0.0.1:5556");
client.Send(Input);
--------------------------------------------
but execute in MT4 got following error:
Unhandled exception 0xE0434352
and I do put the netmq.dll into the same folder as the DLL.

Patrick White said...

Eddy,

"Unhandled" exception should be a hint. Make sure you put error handling code inside all your dll methods so you can get a better error code and thus know what to change to fix the problem in your code.

Error handling is particularly important if you do something risky, like referencing an external dll, or doing a network request that could end up returning an error.

Jonny said...

i need use String[] when i send mutil Symbols in mql4 to C# and i cant find solution for it

thanks u

Patrick White said...

Jonny,

I can see how passing an array of symbol names might be useful for some uses. My initial inclination was to simply suggest concatenating the symbol names with something like a comma, then splitting the strings in C#. That would be the easy way. Too easy.

Instead, I took on the challenge of passing string arrays byref and have updated the code in this blog post to show how to use it. If pointers make you cry, just concatenate / split your strings and be happy. Think of the PassStringArrayByref way as experimental so do your own testing and report back what you find out under real world circumstances.

Jonny said...

Hi Patrick!

Thanks you for suggest. I will try it.

Thanks

Peter Lin said...

Nice sharing, the code is definitely working but I have a same issue as Eddy Lai that can't make third party dll loaded correctly like NetMQ c# dll reference by the dll we created for dll export. No matter where I put the dll. In system folder or mt4 libraries folder. Is there any way to make the export unmanaged dll reference correctly to the dependent third party dlls?

Patrick White said...

Hi Peter,

I'm afraid I'm a bit out of my depth with regards to NetMQ and haven't had experience calling 3rd party dlls from this project (that I'm aware of). As a point of reference, my own separate attempt using ZeroMQ failed in native C#.

Linking in an extra outside DLL is sort of like dancing with three partners. I suppose the key will first identifying which "dance partner" is causing the error to identify the "where", and then getting back a valid error message to understand the "what" to fix.

To your question, "Is there any way to make the export unmanaged dll reference correctly to the dependent third party dlls?" can you post some code showing how you are currently referencing the dependent third party dll in your code? I know within C# you can call windows APIs which are DLLs like this:

[DllImport("User32.dll", EntryPoint = "FindWindow")]
public static extern Int32 FindWindow(String lpClassName, String lpWindowName);

It is possible to use the full path instead of just "User32.dll", so I'm not sure why you can't just create a directory somewhere in a fixed location and include the full path to that dll file as an easy to way to get this done on a single computer. For a more general solution see below:

I'm not sure but maybe this will help for Windows: https://msdn.microsoft.com/en-us/library/4191fzwb(v=vs.110).aspx Let me know what you find out!

donperry said...

Hey,

In MT4 in loads the DLL but says cannot find one of my functions (well, the only one I tested)

[DllExport("ReadPosition", CallingConvention = CallingConvention.StdCall)]
[return: MarshalAs(UnmanagedType.LPWStr)]
public static string ReadPosition(string filelocation)
{
return "";
}


2016.07.31 23:36:20.592 2016.05.24 00:00 Cannot find 'ReadPosition' in 'Mt4test.dll'

donperry said...

EDIT: CPU type x86 fixed it