Index of C# Binding

1. Introduction

As we proposed in the specification the Zeus-Framework is able to handle different modules written in other languages. The big difference between Zeus-Framework and other C++ Frameworks such as STL and boost is, that Zeus is using the interface concept. We will see why interfaces are so important to meet our goal, in this case the binding of modules written in C#.

Note that the following examples are exclusivly written in Visual Studio 2005 from Microsoft. I haven't tested the same thing on Mono for Linux yet.

To see and test the proposed C# binding, download the latest Zeus-Framework. There is a Borland test application witch loads a C# DLL as a module.


2. Setting up the .Net Framework

This chapter shows the basic setups to use the .Net Framework. Some are trivial like DLL import, others are more involved like DLL export.

2.1 DLL Import

The .Net Framework provides a useful functionality to use functions of other libraries. This is similar to the LoadLibrary() functions in Windows. Following example shows the import of SendMessage() from user32.dll:


namespace test
{
  public class A
  {
    [DllImport("user32.dll")]
    public static extern int SendMessage(
          IntPtr hWnd,      // handle to destination window
          uint Msg,       // message
          long wParam,  // first message parameter
          long lParam   // second message parameter
          );
  }
}

2.2 DLL Export

Unfortunately the .Net does not directly support the export of functions. But there is work around to do so. I've found a rather good tool called "ExportDLL" on code project. You might download it also here.

There are two projects included. The first is called "ExportDllAttribute". Make sure the DLL is generated where you want to use your module. Simply add it as a dependency to your module project.

The second project is called "ExportDll". This is an application witch is needed to disassemble, adding the exports and rebuild your project. Add following lines to the post build properties of your project:


"..\..\bin\ExportDll.exe" "$(TargetPath)" /$(ConfigurationName)

Since this tool uses the disassembler and assembler tools of .Net, you might have to configure the ExportDll.exe.cofing file. Adjust the file path and name.


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <sectionGroup name="applicationSettings" 
                     type="System.Configuration.ApplicationSettingsGroup, 
                           System, Version=2.0.0.0, Culture=neutral, 
                           PublicKeyToken=b77a5c561934e089" >
      <section name="ExportDll.Properties.Settings" 
                  type="System.Configuration.ClientSettingsSection, 
                        System, Version=2.0.0.0, Culture=neutral, 
                        PublicKeyToken=b77a5c561934e089" 
                        requirePermission="false" />
    </sectionGroup>
  </configSections>
  <applicationSettings>
    <ExportDll.Properties.Settings>
      <setting name="ildasmpath" 
                  serializeAs="String">
        <value>C:\Programme\Microsoft.NET\SDK\v2.0\Bin\ildasm.exe</value>
      </setting>
      <setting name="ilasmpath" serializeAs="String">
        <value>C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\ilasm.exe</value>
      </setting>
    </ExportDll.Properties.Settings>
  </applicationSettings>
</configuration>

No we are ready to implement some functions to export. Look at the following code:


namespace test
{
  public class TA
  {
    [ExportDllAttribute.ExportDll("getSqrIntValue",
      System.Runtime.InteropServices.CallingConvention.StdCall)]
    public static int getSqrIntValue(int iVal)
    {
      return iVal * iVal;
    }
  }
}

This will export now a function called "getSqrIntValue". Note that the name of the exported function is taken from the String given to the attribute.

To work with Zeus-Framework its important to use the standard call property, since all methods on win32 systems are called using the standard call.

2.3 Other Settings

As I tried to run the whole test application, I had some problems loading my ZeusMarshalling DLL written in managed C++. The problem was that it couldn't find the DLL "MSVCM80.DLL", "MSVCP80.DLL" and "MSVCR80.DLL". The problem occured because the manifest option was set to "no". It's important to generate the manifest file here.

The Zeus-Framework libraries are build without this option. So make sure when using Zeus-Framework within managed C++ this option is turned on.


3. Binding Project

This chapter is about the binding project for C#. First the project structure is explained. Then in a second step the different interfaces are discussed.

3.1 Project structure

The whole project contains following sub projects:

Name Function
ExportDll Generates the exported function. (see 2.2 DLL Export)
ExportDllAttribute Used to mark the functions to export by an attribute. (see 2.2 DLL Export)
zeusbaseWin32 .Net library with interfaces and help classes written in C#
ZeusMarshalling Marshalling of datatypes between C++ and C#. Specially strings and IZUnknown handling is implemented so far.

3.2 Exporting functions

3.2.1 registerLibrary and unregisterLibrary

The Zeus-Framework needs different module functions to be exported. The first two are registerLibrary and unregisterLibrary. Lets look at the register function first. I implemented the exported functions in the module session class of the test module. Some namespaces from Zeus are needed:


using Zeus.MOM;
using Zeus.System;
using Zeus.System.Interfaces;
using Zeus.Marshalling;

namespace test
{
  class TTestModule : TAbstractModuleSession
  {
     ...

    /*********************************************/
    /*! Registers the module 
     */
    /*********************************************/
    [ExportDllAttribute.ExportDll("registerLibrary",
      System.Runtime.InteropServices.CallingConvention.StdCall)]
    public static Retval registerLibrary(
       [MarshalAs(UnmanagedType.Interface)] ISingletonManager rManager)
    {
      Marshal.ReleaseComObject(rManager);
      return Retval.RET_NOERROR;
    }
  }
}

Now lets look at the wierd structure of this method. Fist of course we declare the function as an exported function. The function must also be static of course. Our return value is Retval, witch is an enumeration in C# and corresponds to Retval type inn C++. I changed the values of Retval, such that they are compatible with COM. This does not affect any functionality in Zeus-Framework win32 or linux.

Our parameter passed by the functions is the interface ISingletonManager. The attribute tells the .Net framework, that this parameter is an unmanaged type. Since it's an interface type, the .Net Framework mapped it to a COM interface and therefore I have to release it at the end with Marshal.ReleaseComObject(rManager).

The unregisterLibrary-Function is similar. Since this function is called at the end, I try to clean the memory using the garbage collector.


/*********************************************/
/*! Unregisters the module 
  */
/*********************************************/
[ExportDllAttribute.ExportDll("unregisterLibrary",
  System.Runtime.InteropServices.CallingConvention.StdCall)]
public static Retval unregisterLibrary(
    [MarshalAs(UnmanagedType.Interface)] ISingletonManager rManager)
{
  ///////////////////////////////////////////////
  //This might cause problems starting the Zeus-
  // Application from the Studio environment.
  // but its nessecary to release all COM objects 
  // taken from the Zeus-Framework.
  // If you disable this line, make sure all COM-
  // Objects of any methods (even parameters)
  // are released manualy by calling 
  // Marshal.ReleaseComObject(rParam);
  GC.Collect();
  ///////////////////////////////////////////////

  Marshal.ReleaseComObject(rManager);
  return Retval.RET_NOERROR;
}

3.2.2 Factory functions

Now lets look at the factory functions. There are two different function types. The first simply creates an object and returns its reference. The function name starts allways with "create" followed by the name of the object interface.


/*********************************************/
/*! Factory method to create regular objects 
    (non X-Objects).
  */
/*********************************************/
[ExportDllAttribute.ExportDll("createITestOfIString",
  System.Runtime.InteropServices.CallingConvention.StdCall)]
public static Retval createITestOfIString(
  [MarshalAs(UnmanagedType.Interface)] out ITestOfIString rpObj)
{
  rpObj = new TestOfIString();

  return Retval.RET_NOERROR;
}

The second factory function is used for X-Objects. Explanations will follow as soon as I tested the function.


4. Interface definition

This section shows how to define the interfaces in order to use them in C# and C++ Zeus-Framework. Note that all interfaces defined are inheriting the IZUnknown interface in C++ and IUnknown in C#.

4.1 Using the concept of GUID

Every interface must be identified by a globally unique identifier (GUID). In C# this identifier can be defined using an attribute of the class. Make sure the interface is COM-visible and inherits the IUnknown (InterfaceType(ComInterfaceType.InterfaceIsIUnknown):


using System.Runtime.InteropServices;

namespace CSharpTestModule.Interfaces
{
  /*********************************************/
  /*! This interface is used to test the 
      IString interface on the C# module.
      Note that on the C# interface declaration 
      the interface is inherited 
      from IUnknown (interface type) and it is 
      com visible. Com visible means
      that askForInterface (QueryInterface) will 
      work.
  */
  /*********************************************/
  [Guid("4372B788-EEE7-4ee7-ACC1-FF853C89ED94"),
   InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
  [ComVisibleAttribute(true)]
  public interface ITestOfIString
  {
    ...
  }
}

This GUID is used for example to create objects out of the module session (See chapter Module session.

If you want to use interfaces already defined in Zeus-Framework or other C++ modules (libraries) make sure the GUID is identical with the interface ID defined in the header file.

4.2 Method definition

To define methods is more involved. There are different thing we have to consider:

  • Preserving the signature, since COM normaly uses HRESULT as return value. The attribute [PreserveSig()] is used for each method.
  • Passing unknown data types or non-COM interfaces as IntPtr.
  • References like IString& are passed as pointer as well.
  • Boolean values might be a problem. Use byte as boolean.

Following example gives you an idea how this works. Note that I wrote the C++ signature as comment.


[Guid("4372B788-EEE7-4ee7-ACC1-FF853C89ED94"),
  InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[ComVisibleAttribute(true)]
public interface ITestOfIString
{
  /*********************************************/
  /*! Reads a string value from the object
    */
  /*********************************************/
  [PreserveSig()]
  void getString(/*IString&*/
    [In, MarshalAs(UnmanagedType.SysInt)] IntPtr rValue);

  /*********************************************/
  /*! Sets the string value to the object
    */
  /*********************************************/
  [PreserveSig()]
  void setString(/*const IString&*/
    [In, MarshalAs(UnmanagedType.SysInt)] IntPtr rValue);

  /*********************************************/
  /*! Sets the string delegater
  */
  /*********************************************/
  [PreserveSig()]
  void setDelegater(/*ITestOfIStringDelegate&*/
    [In, MarshalAs(UnmanagedType.Interface)] ITestOfIStringDelegate rDelegater);

  /*********************************************/
  /*! Sets the string delegater
  */
  /*********************************************/
  [PreserveSig()]
  Retval getDelegater(/*ITestOfIStringDelegate*&*/
    [Out, MarshalAs(UnmanagedType.Interface)] out ITestOfIStringDelegate rpDelegater);
}


5. Marshalling

Since not all interfaces of the Zeus-Framework are COM like, we need a mapping (marshalling) of some data structures. To do the mapping I created a marshaller library written in managed C++ depending of the ZeusFramework library compiled for Visual Studio 2005 (C++ Project).

5.1 Marshalling of IString

To access to a parameter of type IString, I suggest to use simply an IntPtr (witch is similar to void* in C++). I set the attribute of the parameter to [In, MarshalAs(UnmanagedType.SysInt)] as well. But how can I access the data of the IString-Parameter? Lets look at a small example here. Our test class TestOfIString inhertits the interface ITestOfIString. Methods of this interface are called by libraries written in C++



C++ side:  
  TAutoPtr<ITestOfIString> ptrObject; 

  TString strData1;
  ptrObject->getString(strData1);
  if (strData1 == L"Test") ...


C# side:

  //using directive for Zeus-Framework binding
  using Zeus.Marshalling;
  using Zeus.System;

  using CSharpTestModule.Interfaces;

  namespace CSharpTestModule
  {
    /*********************************************/
    /*! This is the concrete implementation of the 
        test class for IString
        functionality.
    */
    /*********************************************/
    public class TestOfIString : ITestOfIString
    {
      /*********************************************/
      /*! \see ITestOfIString::getString
      */
      /*********************************************/
      public void getString(/*IString&*/
        [In, MarshalAs(UnmanagedType.SysInt)] IntPtr rValue)
      {
        TMarshalOfIString.assignStr(rValue, m_strData);
      }
    }
  }

Well since IString is just a data-container, I created a marshalling class between IString and the managed string of C#, the TMarshalOfIString. This class provides method to access IString's basically coping data from C# strings to IString's and via verse. Following methods are available at the moment:


  static /*TString*/ IntPtr create();
  static /*TString*/ IntPtr create(int iValue);
  static /*TString*/ IntPtr create(unsigned int uiValue);
  static /*TString*/ IntPtr create(float fValue);
  static /*TString*/ IntPtr create(double dValue);
  static /*TString*/ IntPtr create(String^ strData);
  static void destroy(/*TString*/ IntPtr pPtr);

  //
  static String^ c_bstr(/*IString*/ IntPtr pPtr);
  static void assignStr(/*IString*/ IntPtr pPtr, String^ strData);

Creating new IString-objects are sometimes needed to pass data to a interface of Zeus-Framework. Make sure you destroy the object even in C#, since the IString's are not managed:



/*********************************************/
/*! \see ITestOfIString::setDelegater
  */
/*********************************************/
public void setDelegater([In, MarshalAs(UnmanagedType.Interface)] 
  ITestOfIStringDelegate rDelegater)
{
  IntPtr pData = TMarshalOfIString.create();
  rDelegater.readSomeText(pData);

  m_strData = TMarshalOfIString.c_bstr(pData);

  //Release all marshalled adapter objects
  TMarshalOfIString.destroy(pData);

  //Must be done manualy, since the GC will release the 
  // object to late, if you run the module in 
  // visual studio environment.
  Marshal.ReleaseComObject(rDelegater);
}

5.2 Marshalling of IUnknown

The class TMarshalOfIZUnknown implements some methods used to handle IZUnknown interfaces correctly. Some of the functionality can also be achieved by the Marshal class of C#, such as release() and addRef().


  static void addRef(IntPtr pPtr);
  static void release(IntPtr pPtr);
  static void release(Object^ pPtr);
  static Retval askForInterface(IntPtr pPtr, Guid% rGuid, IntPtr% rpIface);
  static Object^ askForInterface(Object^ pPtr, Guid% rGuid);
  
  static Guid getInterfaceIDFromPtr(IntPtr pPtr);
  
  static bool isEqualInterface(Guid% Guid1, IntPtr pPtr);
  static IntPtr getIZUnknownPtr(Object^ pObject);
  static IntPtr getIZUnknownPtr(Guid% rGuid, Object^ pObject);


6. Module session

At the end let complete our module session. Combining all the concepts above we are able now to write the method createObjectOfModule() to create C# objects for Zeus-Framework and other C++ modules.


namespace test
{
  class TTestModule : TAbstractModuleSession
  {
     ...

    /*********************************************/
    /*! \see IModuleSession::createObjectOfModule
     */
    /*********************************************/
    public override Retval createObjectOfModule(
      /*const InterfaceID&*/
      [In, MarshalAs(UnmanagedType.SysInt)] IntPtr rInterfaceID,
      /*IZUnknown*&*/ 
      [Out, MarshalAs(UnmanagedType.SysInt)] out IntPtr rpIface)
    {
      Retval retValue = Retval.RET_UNKNOWN_INTERFACE;

      Guid pGuid = new Guid("4372B788-EEE7-4ee7-ACC1-FF853C89ED94");
      if (TMarshalOfIZUnknown.isEqualInterface(ref pGuid, rInterfaceID))
      {
        rpIface = TMarshalOfIZUnknown.getIZUnknownPtr(ref pGuid, new TestOfIString());
        retValue = Retval.RET_NOERROR;
      }
      else
      {
        rpIface = IntPtr.Zero;
      }

      return retValue;
    }

Note that I have to return an IZUnknown*& witch is basically a generic pointer, but nevertheless I have to cast the pointer to its correct address. To do so I implemented the method TMarshalOfIZUnknown.getIZUnknownPtr().


7. Conclusion

Zeus-Framework is able to handle modules written in C#. This shows how strong the interface concept of Zeus-Framework is.

For software development this makes the framework very interesting. Since new languages and development environments are put to market every year, you are able to use them, develop new modules using the advantages of the new tools, but you are able to integrate them rather easily to your existing product.


8. References

Lots of information I've found at the MSDN of Microsoft:

Tool to export functions: