Exploring Interop Communication

 

1. Introduction

Believe it or not, there are still many projects for which original development began in the 1990’s. This is mostly because it is impossible to completely migrate from old programming languages and environments. So to manage that, a good idea might be to develop and integrate new features written in the .NET framework. There are several ways of running .NET code from native code, but in this article we will focus on two methods:

·        C++/CLI

·        Unmanaged Exports

The figure below presents an architecture of our example. In this post, we are going to standardize the method of logging in the entire application by using single library called Intercom.Logger. That library uses a popular log4net library for writing log messages. Intercom.Logger can be successfully used in any managed code, but we are going to use it in C++ application.

Figure 1 - Architecture diagram

2. Creating an Intercom solution

For the purpose of this article it is necessary to create a new solution named Intercom with two projects: Intercom.Logger and Intercom.Client.

Intercom.Logger

Intercom.Logger is a managed library responsible for appending the log file. For this purpose, an external library called log4net will be used.

1.      Open the Visual Studio and create a new solution named Intercom with the Intercom.Logger class library project.

 

Figure 2 - Creating Intercom solution and Intercom.Logger Class Library.

2. Install log4net for Intercom.Logger project by using NuGet.

 

Figure 3 - Log4Net reference.

3. Create the following class in Intercom.Logger project.

namespace Intercom.Logger

{

    using log4net;

    using log4net.Appender;

    using log4net.Core;

    using log4net.Layout;

    using log4net.Repository.Hierarchy;

 

    public static class Logger

    {

        private static readonly ILog MyLog;

 

        static Logger()

        {

            var hierarchy = (Hierarchy)LogManager.GetRepository();

 

            PatternLayout patternLayout = new PatternLayout();

            patternLayout.ConversionPattern = "%date [%thread] %-5level %logger - %message%newline";

            patternLayout.ActivateOptions();

 

            FileAppender appender = new FileAppender();

            appender.File = @"Logs.txt";

            appender.Layout = patternLayout;

            appender.ActivateOptions();

 

            hierarchy.Root.AddAppender(appender);

 

            hierarchy.Root.Level = Level.Info;

 

            hierarchy.Configured = true;

 

            MyLog = LogManager.GetLogger(string.Empty);

        }

 

        public static void Log(string message)

        {

            MyLog.Info(message);

        }

    }

}

 

 

Figure 4 - Logger class.

Intercom.Client

The Intercom.Client is a native Win32 application that needs a logging feature.

1. In the solution created in the previous section, add a new C++ Win32 Console Application project named Intercom.Client.

Figure 5 - Intercom.Client

2. Include Windows.h in the stdafx.h header file.

// stdafx.h : include file for standard system include files,

// or project specific include files that are used frequently, but

// are changed infrequently

//

 

#pragma once

 

#include "targetver.h"

 

#include <stdio.h>

#include <tchar.h>

 

 

 

// TODO: reference additional headers your program requires here

#include <Windows.h>

 

Figure 6 - stdafx.h header file content.

3. Creating interop projects

After creating the Intercom solution, there are two independent components:

·        Intercom.Client unmanaged native Win32 Console Application

·        Intercom.Logger managed .NET Framework Class Library

The goal is to use the logging feature exposed by Intercom.Logger library in the  Intercom.Client application. Intercom.Client is a native unmanaged application so it cannot directly reference and call Intercom.Logger. It is necessary to create some kind of interop/proxy library.

Figure 7 - Already implemented components.

Intercom.Logger.UnmanagedExports

Unmanaged exports is one way of exposing managed C# code to unmanaged native code. The main idea is to decompile an already compiled module into IL code, change module's VTable and VTableFixup tables and recompile the DLL. This operation can be performed invisibly to the developer by using NuGet package called UnmanagedExports. That package contains library with DllExport attribute and MSBuild project that automatically changes module's tables.

1. In Intercom solution add a new C# Class Library project called Intercom.Logger.UnmanagedExports.

Figure 8 - Adding Intercom.Logger.UnmanagedExports project.

2. For a newly created project install a NuGet package called UnmanagedExports.

Figure 9 - Installing UnmanagedExports package.

3. Add Intercom.Logger reference.

 

Figure 10 - Adding Intercom.Logger reference.

4. Create a LoggerUnmanagedInterop proxy class.

The method exposed to unmanaged native code must be decorated with the DllExport attribute.

namespace Intercom.Logger.UnmanagedExports

{

    #region Usings

    using System.Runtime.InteropServices;

    using RGiesecke.DllExport;

    #endregion

 

    public static class LoggerUnmanagedInterop

    {

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

        public static void Log(

            [MarshalAs(UnmanagedType.LPStr)] string message)

        {

            Logger.Log(message);

        }

    }

}

 

Figure 11 - LoggerUnmanagedInterop class.

5. Add the following post-build event command to copy binaries to the Intercom.Client output directory.

copy $(TargetDir)*  $(SolutionDir)$(Configuration)\

 

Figure 12 - Adding post-build event.

6. In the build settings, change the platform target to x86 for All Configurations.

7. Edit the Interop.Client.cpp main procedure and call method exposed by UnmanagedExports assembly.

#include "stdafx.h"

 

// Define the type of exposed procedure.

typedef VOID (CALLBACK* LOG)(char message[]);

 

int _tmain()

{

       // Load Intercom.Logger.UnmanagedExports.dll

       HMODULE hUnmanagedExports = LoadLibraryW(TEXT("Intercom.Logger.UnmanagedExports.dll"));

       if (hUnmanagedExports == NULL){

             printf_s("Failed to load Intercom.Logger.UnmanagedExports.dll");

             return -1;

       }

 

       // Load ManagedLogger_Log procedure.

       LOG log = (LOG)GetProcAddress(hUnmanagedExports, "LoggerUnmanagedInterop_Log");  

       if (log == NULL){

             printf_s("The address of LoggerUnmanagedInterop_Log procedure is unknown");

             return -1;

       }

 

       // Call exposed procedure.

       log("Message to be logged");

 

       return 0;

}

 

Figure 13 - Using exposed managed procedure in unmanaged C++ code.

8. Build the solution.

9. Navigate to the Release/Debug folder in the root solution directory. The directory should contain Intercom.Client.exe and files copied by build-event of Intercom.Logger.UnmanagedExports library.

Figure 14 - Release/Debug directory.

After executing Intercom.Client.exe the log file should be created.

Figure 15 - Execution result.

From now the native program Intercom.Client uses the logging feature from Intercop.Logging by calling the procedure exported in Intercom.Logger.UnmanagedExports.

The figure below shows how the architecture was implemented by using UnmanagedExports. Intercom.Logger.UnmanagedExports can be used in any language that supports dynamic DLL loading: C, C++, Clarion, VBA and so on.

Figure 16 - Already implemented components.

Intercom.Logger.CLI

The second way of exposing .NET methods for native code is by creating a C++/CLI library. C++/CLI is a language specification created and intended to supersede Managed Extensions for C++. It can be said that the library written in C++/CLI is a kind of hybrid of C++ and C#.

1. Add a new project called Intercom.Logger.CLI

Figure 17 - Adding CLI project.

2. Add Intercom.Logger reference.

Figure 18 - Adding Intercom.Logger reference.

Figure 19 - Adding Intercom.Logger reference.

3. Add CliLog function header to the Intercom.Logger.CLI.h file

#pragma once

 

using namespace System;

 

extern "C" _declspec(dllexport)  void __stdcall CliLog(char message[]);

 

Figure 20 - Intercom.Logger.CLI.h header file.

4. Implement CliLog function in Intercom.Logger.CLI.cpp file

#include "stdafx.h"

 

#include "Intercom.Logger.CLI.h"

 

void __stdcall CliLog(char message[])

{     

       Intercom::Logger::Logger::Log(gcnew String(message));

}

 

Figure 21 - CliLog function implementation.

5. Create a file with the export procedures definitions named Exports.def with the following content:

LIBRARY Intercom.Logger.CLI.DLL

EXPORTS

   CliLog          @4

 

Figure 22 - Exports.def file content.

That file contains definitions of procedures that will be exported. The @4 expression represents the number of bytes taken in arguments. In this case message[] parameter is 32 bit pointer, so it's size is equal to 4 bytes.

Make sure the file properties look like the following:

Figure 23 - Exports.def file properties.

The Exports.def file should be also included in project properties like in the figure below:

Figure 24 - Including Exports.def in project properties.

6. Add the following code to the Intercom.Client.cpp:

// Load Intercom.Logger.CLI.dll

HMODULE hCli = LoadLibraryW(TEXT("Intercom.Logger.CLI.dll"));

if (hCli == NULL){

       printf_s("Failed to load Intercom.Logger.CLI.dll");

       return -1;

}

 

// Load Log procedure.

LOG logCli = (LOG)GetProcAddress(hCli, "CliLog");   

if (logCli == NULL){

       printf_s("The address of Log procedure is unknown");

       return -1;

}

 

// Call exposed CLI procedure.

logCli("Message from CLI");

 

7. Rebuild and run the Intercom.Client project.

CLI and client binaries will be automatically copied to the same directory. It is not required to add any post-build events in this case. After running, the file Logs.txt should contain also the message added by calling CliLog() export.

The figure below shows the already implemented features. This time the Intercom.Client application uses the CLI library to execute managed code from Intercom.Logger.

Figure 25 - Architecture diagram.

4. Summary

The main goal of this article was to show how to call managed .NET code from an unmanaged native C++ application. The examples are very simple but may prepare a good groundwork for extending enterprise applications written in C++/Clarion/VBA or other languages which may benefit from DLL exports. The Log() procedure contains only one argument: char array, but it is possible to pass other types and entire structures. More information can be found in the sites listed below, and you can also find a link to the sources used in this article as well:

DLL Export Viewer can be useful for browsing DLL exports:
http://www.nirsoft.net/utils/dll_export_viewer.html

More information about UnmanagedExports:

https://sites.google.com/site/robertgiesecke/Home/uploads/unmanagedexports

https://www.nuget.org/packages/UnmanagedExports

 

To see the sources for this project, download the zip file of all documents here.

 

Want to build your desktop, mobile or web applications with high-performance controls? Download Ultimate Free trial now and see what it can do for you!