Exporting .NET DLLs with Visual Studio 2005 to be Consumed by Native Applications

Tuesday Jan 22nd 2008 by Boian Petkantchin
Share:

Learn how to create a DLL, implementing the Stdcall calling convention. This DLL wraps a .NET DLL, which then is imported in MetaTrader 4 through the wrapper.

Abstract

This article will explain how to create a wrapper for a .NET DLL and import it in MetaTrader 4. I used Visual Studio 2005 and wrote the example .NET DLL in C# and the wrapper in C++. The wrapper is again a DLL, which implements the Stdcall calling convention (MetaTrader 4 supports only this convention but you can produce a DLL with any calling convention as long as .NET supports it). The .NET DLL can be easily written in any other .NET language. The technique described below can be used to export .NET DLLs to be used by any unmanaged applications.

Motivation

When I decided to create an expert advisor for MetaTrader to test a trading strategy, I was frustrated because of many unsuccessful attempts to find out how to create DLLs and import them in the expert advisor. I am coming from a Java background and a year and a half of programming in C#. I have a slight knowledge of C++ and feel like I am crippled when I have to write something in it. Because of that, my first decision was to implement the trading strategy in C#. I spent days trying to figure out how, but my nerves couldn't stand it, so I gave it all up and switched to Visual C++ 6.0. When I realized how quickly I am in writing C++ code, I began having second thoughts and started trying in .NET again. After days of trial and error, I came up with the following step-by-step guide.

Step-by-Step Guide for Creating a Sample DLL

I. Creating the C# DLL

  1. Create a new C# class library project called CSharpAssembly in a new solution named DotNetDllForMetaTrader4.
  2. Add the following C# class file to the project:
  3. CSharpClass.cs:
    using System;
    using System.Collections.Generic;
    using System.Text;
    
    namespace CSharpAssembly
    {
       public class CSharpClass
       {
          public static byte[] Hello(byte[] name)
          {
             string s = ", hello from .NET!";
             byte[] helloPart = Encoding.ASCII.GetBytes(s);
             byte[] whole =
                new byte[name.Length + helloPart.Length];
             int i = 0;
             foreach (byte b in name)
             {
                whole[i++] = b;
             }
             foreach (byte b in helloPart)
             {
                whole[i++] = b;
             }
          return whole;
          }
       }
    }
    
  4. Change the CSharpAssembly project -> Properties -> Build -> Output path field to "..\debug". You do this to dump all the output to one folder.
    This step should be completed after adding CppStdcallInerfaceWrapper project to the solution because this will automatically change the solution platform from Any CPU to Mixed Platforms.

II. Creating the C++ Wrapper, Implementing the Stdcall Calling Convention

  1. Add to the solution a new C++ Win32 project called CppStdcallInerfaceWrapper with application type DLL.
  2. Change the CppStdcallInerfaceWrapper.cpp file content to:
  3. #include "stdafx.h"
    
    #ifdef _MANAGED
    #pragma managed(push, off)
    #endif
    
    #ifdef _MANAGED
    #pragma managed(pop)
    #endif
    
    #using "CSharpAssembly.dll"
    using namespace CSharpAssembly;
    
    __declspec(dllexport) char* __stdcall Hello(char* name)
    {
       int i = 0;
       while (*name != '\0')
       {
          i++;
          name++;
       }
       array<unsigned char>^ nameManArr =
          gcnew array<unsigned char>(i);
       name -= i;
       i = 0;
       while (*name != '\0')
       {
          nameManArr[i] = *name;
          name++;
          i++;
       }
       array<unsigned char>^ char8ManArr =
          CSharpClass::Hello(nameManArr);
       char*  char8UnmanArr = new char[char8ManArr->Length + 1];
       for (int i = 0; i < char8ManArr->Length; i++)
       {
          char8UnmanArr[i] = char8ManArr[i];
       }
       char8UnmanArr[char8ManArr->Length] = '\0';
       return char8UnmanArr;
    }
    
  4. To make the Hello function visible in the CppStdcallInerfaceWrapper.dll through the Stdcall calling convention, add a new Module Definition file called CppStdcallInerfaceWrapper.def in the project source files. This will automatically set the project properties -> Linker -> Input -> Module Definition file.
  5. To add Hello to the list of exported functions, change the CppStdcallInerfaceWrapper.def content to the following:
  6. LIBRARY "CppStdcallInerfaceWrapper"
    EXPORTS
       Hello
    
  7. To call the CSharpAssembly.dll, go to project properties -> Configuration Properties -> General and add Common Language Runtime Support.
  8. To enable the compiler to locate the CSharpAssembly.dll, go to project properties -> C/C++ -> General -> Resolve #using References and add "$(SolutionDir)\debug".

III. Creating a Debug Entry Point Project

  1. Add to the solution a new C# console application project called DebugEntry.
  2. Add the following C# class file to the project:
  3. DebugEntry.cs:
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Runtime.InteropServices;
    
    namespace DebugEntry
    {
       class DebugEntry
       {
          [DllImport("CppStdcallInerfaceWrapper.dll",
                CharSet = CharSet.Ansi, CallingConvention =
                   CallingConvention.StdCall)]
            public static extern string Hello(string name);
    
            static void Main(string[] args)
            {
                System.Console.WriteLine(Hello("MyName"));
                System.Console.ReadLine();
            }
       }
    }
    
  4. Change the DebugEntry project -> Properties -> Build -> Output path field to "..\debug". You do this to dump all the output to one folder.
  5. In solution properties -> Common Properties -> Startup Project, change the single startup project to DebugEntry.

IV. Setting Project Dependencies and Building the Solution

  1. Go to solution properties -> Common Properties -> Project Dependencies and set the dependencies as follows:
    DebugEntry depends on CppStdcallInterfaceWrapper and CSharpAssembly,
    CppStdcallInterfaceWrapper depends on CSharpAssembly.
  2. Build the solution.

V. Testing the DLLs in MetaTrader 4

  1. Add to the Path environment variable the MetaTrader 4 main directory. Example: "C:\Program Files\MetaTrader 4". You can do it through My Computer -> Properties -> Advanced -> Environment Variables. You do this so MetaTrader 4 can locate your DLLs.
  2. Copy CSharpAssembly.dll and CppStdcallInerfaceWrapper.dll from "$(SolutionDir)\debug" to MetaTrader's main directory.
    The reason you don't put the DLLs in "C:\Program Files\MetaTrader 4\experts\libraries", where external DLLs for MetaTrader usually reside is that when the calling order is MetaTrader 4 Terminal -> CppStdcallInerfaceWrapper.dll -> CSharpAssembly.dll, it will not locate the CSharpAssembly.dll. The Path environment variable can't solve this because the CLR doesn't use it to locate assemblies. That's why I put the DLLs where the MetaTrader terminal executable is. In the Reference section below, there is a link covering the matter on runtime assembly location.
  3. In the MetaTrader platform, create a new expert advisor called DotNetDllTest.
  4. Before the init() function in DotNetDllTest.mq4, insert the following:
  5. #import "user32.dll"
    int MessageBoxA(int hWnd ,string szText,
                    string szCaption, int nType);
    
    #import "CppStdcallInerfaceWrapper.dll"
    string Hello(string name);
    
  6. At the beginning of the init() function, insert:
  7. MessageBoxA(100, Hello("MyName"), "", 0);
    The user32.dll is a DLL that comes with the Windows operating system; you use it just to show a box with the hello message.
  8. Run the DotNetDllTest expert advisor with Allow DLL imports option on.

Explanations about the Step-by-Step Guide

The DebugEntry project is not necessary, but with it you can debug other projects. To create a release version, all the properties setting steps must be replicated. In the Hello function, project CppStdcallInterfaceWrapper, the char8UnmanArr array is unmanaged and you have to deallocate it, using the delete operator, after you are finished using it in MetaTrader. To keep things simple, this is not done in the example above. You can do it by writing another function in the CppStdcallInterfaceWrapper and calling it from MetaTrader.

Another way of passing data to the unmanaged MetaTrader is not by creating an unmanaged copy, but by using the System.InteropeServices.GCHandle to pin the needed managed object while using it in the unmanaged MetaTrader, so the Garbage Collector doesn't move or delete it.

Other thing you sould take care of is the ecoding convention. .NET strings are in Unicode, whereas the calling application can support only ASCII, like with MetaTrader.

Security Issues

Although there are several available ways to protect your .NET DLLs from back engineering, this is not as secure as native byte code. References to other materials about this matter are provided in the next section.

References

Share:
Home
Mobile Site | Full Site
Copyright 2017 © QuinStreet Inc. All Rights Reserved