Showing posts with label C# DLL. Show all posts
Showing posts with label C# DLL. Show all posts

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.

Tuesday, April 3, 2012

Code to Export C# DLL to Metatrader

Note: updated build 600+ code can be found at Code to Export C# DLL to Metatrader Build 600+.

I worked up a simple sample C# DLL that is properly exported for use with Metatrader. I made use of the template that can be downloaded from C# Project Template for Unmanaged Exports.

C# Code for "testUMD.dll" below using R. Giesecke's template:
Code:
using System;
using System.Text;
using RGiesecke.DllExport;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace testUnmanagedDLL
{
   class Test
   {
 
        [DllExport("AddInteger", CallingConvention = CallingConvention.StdCall)]
        public static int AddInteger(int Value1, int Value2) {
            MessageBox.Show("Add Integers: " + Value1.ToString() + " " + Value2.ToString());
            return (Value1 + Value2);
        }

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

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

        [DllExport("returnString", CallingConvention = CallingConvention.StdCall)]
        public static string returnString(string Input) {
            MessageBox.Show("Received: " + Input);
            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;
        }

   }


MT4 Script 'testDLL' code below: File testUMD.dll added to experts/libraries folder
Code:
#import "testUMD.dll"
   int AddInteger(int Value1, int Value2);
   double AddDouble(double Value1, double Value2);
   string AddDoubleString(double Value1, double Value2);
   string returnString(string Input);
   double ReturnDouble2(); 
#import
//+------------------------------------------------------------------+
//| script program start function                                    |
//+------------------------------------------------------------------+
int start()
  {
//----
   Print("AddInteger: " + AddInteger(250, 750));
   double a = AddDouble(250,750);
   Print("AddDouble: " + NormalizeDouble(a,4));

   double d = StrToDouble(AddDoubleString(250, 750));
   Print("AddDoubleString: " + NormalizeDouble(d,4));
   string temp = "Send to DLL";
   string recv = returnString(temp);
   Print(recv);
   double dd = ReturnDouble2();
   Print("Returning Double from C#: " + NormalizeDouble(dd, 4));
//----
   return(0);
  }

Cursory Summary: Integer passing = success, double passing MT4 to C# = success, double passing from C# to MT4 = fail, string passing = success. I haven't experimented with arrays. Sample code written with VS 10 .NET 4.0.

If anyone knows how to successfully get doubles to pass from C# to MT4 without conversion to a string, please leave a comment!

------------
Edit: thanks for the feedback from anonymous! The code has been updated to reflect passing a double from C# to Metatrader.

By the way, you must use the 32 bit version of the DLL. The 64 bit version and the "Any CPU" version both failed with error #127 in Metatrader. This is understandable considering MT4 is a 32 bit program. 

As per g3ro's suggestion I have made available for download both the testUnamanagedDLL Visual Studio solution and the testDLL MT4 script. Download testUnmanagedDLL VS solution

Monday, April 2, 2012

Export C# DLL to Metatrader

I've recently learned several methods for exporting a C# DLL for use outside .NET and specifically to Metatrader. The first method was alluded to in the article:
Write CSharp (C#) DLL for Metatrader.

This is a template created to make things simpler.

C# Project Template for Unmanaged Exports

The template method works but you have to delete the default function and use a slightly different syntax. (see next post).

I've seen two other articles that I haven't yet had time to fully explore but that I've heard work for simplifying the export of a C# DLL.

Simple Method of DLL Export without C++/CLI

How to Automate Exporting .NET Function to Unmanaged Programs

This second article looks not too difficult and I've heard good feedback on its implementation.

Tuesday, August 2, 2011

Write C# DLL for Metatrader (CSharp)

Those C# developers who would like to write a DLL in C# for Metatrader instead of in C++ now have a path to that end. There is a very interesting article titled "Exposing C# code to MQL5 using unmanaged exports" that I've just read. I use MT4 (MQL4) instead but the idea is still valid.

Read/skim the article and read section 2.5 onward.
Exposing C# code to MQL5 using unmanaged exports - MQL5 Articles

Read the directions, then download the Template for C# "Unmanaged Export Library" and place the zip file in the right directory (by following the directions on that page):
C# Project Template for Unmanaged Exports

Load Microsoft Visual Studio 2010 (or  Microsoft Visual C# 2010 Express) and select the "Unmanaged Export Library" template. Follow the code samples. Hope this helps somebody!

You can find sample code for creating a C# dll with the Unmanaged Export Library here  including  a download for the visual studio solution and the MT4 sample script.

Monday, August 1, 2011

How To Write a C# 2010 DLL and Call It Within VB6 - CSharp Interoperability

Interoperability in C# is a real PITA. I know because I just spent the entire day chasing down a simple example. Unfortunately the C# documentation out there is so sparse and out of date (in this area the C# language seems to have changed quite a bit pre 2010) on the subject that doing a simple task like linking a C# dll from VB6 seems next to impossible. Maybe the topic is so mind numbingly simple that only pure C# neophytes struggle with something so mundane. But there is hope at the end of this rainbow. This short tutorial will give you the code necessary to write a simple C# 2010 DLL, and link that DLL via COM in VB6.

C# code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;


namespace testMath
{
    public interface Math
    {
        int Multiply(int x, int y);
        int Add(int x, int y);
    }


    [ClassInterface(ClassInterfaceType.None)]
    public class Calc : Math
    {
        public int Multiply(int x, int y)
        {
            return(x * y);
        }


        public int Add(int x, int y)
        {
            return (x + y);
        }
    }
}
The public interface is what is shown in VB6 when you look at the referenced class's methods and properties. If the interface is excluded, the class shows up as not having any way to access the methods (interface-less).

The only real kicker is the [ClassInterface(ClassInterfaceType.None)] line that eliminates extraneous class interfaces, including the desired Multiply and Add functions from showing up in the interface. You can verify this by making the removing "public" from in front of the interface declaration.

However, because a public interface is declared with Multiply and Add, those interface elements show up in VB6 as if they were the actual functions, and because the class that inherits the ": Math" interface also contains the Multiply and Add functions, VB6 is none the wiser.

On the AssemblyInfo.cs page make sure you see the following line:
[assembly: ComVisible(true)]

If it is set to false, make the change. This can also be done from within the Visual Studio environment under the Project/Properties menu. Choose to "Register for COM interop" on the Build tab, and on the Application tab under Assembly Information, make sure to check that "Make assembly COM-Visible" is checked.

Note: the C# DLL was saved as testClass.dll.


VB6 code:

Option Explicit
Private o As testClass.Calc
Private Sub Form_Load()
    Set o = New testClass.Calc
    Debug.Print "testClass.Calc"
    Debug.Print o.Add(5, 10)
    Debug.Print o.Multiply(5, 5)
    Set o = Nothing
End Sub

Yes the VB6 end of the equation really is that simple. In the VB6 IDE, just add the reference to the .tlb that was created in C# under Project/References (menu) and you're good to go.