web analytics

Calling unmanaged DLL functions from C#

Options

codeling 1602 - 6666
@2016-02-15 09:06:51

There are two ways that C# code can directly call unmanaged code:

  1. Directly call a function exported from traditional C-Style Win32 DLLs/EXEs.
  2. Call an interface method on a COM object.

For both techniques, you must provide the C# compiler with a declaration of the unmanaged function, and you may also need to provide the C# compiler with a description of how to marshal the parameters and return value to and from the unmanaged code.

Calling a function exported from a traditional Win32 DLL

Platform Invocation Services (PInvoke) provides a way for managed code to call unmanaged functions that are implemented in traditional Win32 (non-COM) DLLs. PInvoke is basically composed of two key members.

  1. The DllImport attribute is a .NET class type that wraps low-level LoadLibrary() and GetProcAddress() calls on your behalf, it shields the .NET developer from the task of directly locating and invoking the exact function export. 
  2. System.Runtime.InteropServices.Marshal is the other key PInvoke-centric type, and it allows you to transform various primitives (including COM types) from managed to unmanaged equivalents and vice versa.

To declare a method as having an implementation from a DLL export, do the following:

  • Declare the method with the static and extern C# keywords.
  • Attach the DllImport attribute to the method. The DllImport attribute allows you to specify the name of the DLL that contains the method. The common practice is to name the C# method the same as the exported method, but you can also use a different name for the C# method.
  • Optionally, specify custom marshaling information for the method's parameters and return value, which will override the .NET Framework default marshaling.

The following example shows you how to use the DllImport attribute to output a message by calling puts from msvcrt.dll.

// PInvokeTest.cs
using System;
using System.Runtime.InteropServices;

class PlatformInvokeTest
{
    [DllImport("msvcrt.dll")]
    public static extern int puts(string c);
    [DllImport("msvcrt.dll")]
    internal static extern int _flushall();

    public static void Main()
    {
        puts("Test");
        _flushall();
    }
}

The preceding example shows the minimum requirements for declaring a C# method that is implemented in an unmanaged DLL. The method PlatformInvokeTest.puts is declared with the static and extern modifiers and has the DllImport attribute which tells the compiler that the implementation comes from msvcrt.dll, using the default name of puts. To use a different name for the C# method such as putstring, you must use the EntryPoint option in the DllImport attribute, that is:

[DllImport("msvcrt.dll", EntryPoint="puts")]

Default Marshaling and Specifying Custom Marshaling for Parameters to Unmanaged Methods

When calling an unmanaged function from C# code, the common language runtime must marshal the parameters and return values.

For every .NET Framework type there is a default unmanaged type, which the common language runtime will use to marshal data across a managed to unmanaged function call. For example, the default marshaling for C# string values is to the type LPTSTR (pointer to TCHAR char buffer). You can override the default marshaling using the MarshalAs attribute in the C# declaration of the unmanaged function.

The following example uses the DllImport attribute to output a string. It also shows you how to override the default marshaling of the function parameters by using the MarshalAs attribute.

// Marshal.cs
using System;
using System.Runtime.InteropServices;

class PlatformInvokeTest
{
    [DllImport("msvcrt.dll")]
    public static extern int puts(
        [MarshalAs(UnmanagedType.LPStr)]
        string m);
    [DllImport("msvcrt.dll")]
    internal static extern int _flushall();

    public static void Main()
    {
        puts("Hello World!");
        _flushall();
    }
}

Output

When you run this example, the string,

Hello World!

will display at the console.

In the preceding example, the default marshaling for the parameter to the puts function has been overridden from the default of LPTSTR to LPSTR.

The MarshalAs attribute can be placed on method parameters, method return values, and fields of structs and classes. To set the marshaling of a method return value, place the MarshalAs attribute in an attribute block on the method with the return attribute location override. For example, to explicitly set the marshaling for the return value of the puts method:

...
[DllImport("msvcrt.dll")]
[return : MarshalAs(UnmanagedType.I4)]
public static extern int puts(
... 

Note   The In and Out attributes can be used to annotate parameters to unmanaged methods. They behave in a similar manner to the in and out modifiers in MIDL source files. Note that the Out attribute is different from the C# parameter modifier, out. For more information on the In and Out attributes, see InAttribute Class and OutAttribute Class.

Specifying Custom Marshaling for User-Defined Structs

You can specify custom marshaling attributes for fields of structs and classes passed to or from unmanaged functions. You do this by adding MarshalAs attributes to the fields of the struct or class. You must also use the StructLayout attribute to set the layout of the struct, optionally to control the default marshaling of string members, and to set the default packing size.

The following example demonstrates how to specify custom marshaling attributes for a struct.

Consider the following C structure:

typedef struct tagLOGFONT
{
   LONG lfHeight;
   LONG lfWidth;
   LONG lfEscapement;
   LONG lfOrientation;
   LONG lfWeight;
   BYTE lfItalic;
   BYTE lfUnderline;
   BYTE lfStrikeOut;
   BYTE lfCharSet;
   BYTE lfOutPrecision;
   BYTE lfClipPrecision;
   BYTE lfQuality;
   BYTE lfPitchAndFamily;
   TCHAR lfFaceName[LF_FACESIZE];
} LOGFONT; 

In C#, you can describe the preceding struct by using the StructLayout and MarshalAs attributes as follows:

// logfont.cs
// compile with: /target:module
using System;
using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Sequential)]
public class LOGFONT
{
    public const int LF_FACESIZE = 32;
    public int lfHeight;
    public int lfWidth;
    public int lfEscapement;
    public int lfOrientation;
    public int lfWeight;
    public byte lfItalic;
    public byte lfUnderline;
    public byte lfStrikeOut;
    public byte lfCharSet;
    public byte lfOutPrecision;
    public byte lfClipPrecision;
    public byte lfQuality;
    public byte lfPitchAndFamily;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst=LF_FACESIZE)]
    public string lfFaceName;
}

The structure can then be used in C# code as shown below:

// pinvoke.cs
// compile with: /addmodule:logfont.netmodule
using System;
using System.Runtime.InteropServices;

class PlatformInvokeTest
{  
      [DllImport("gdi32.dll", CharSet=CharSet.Auto)]
      public static extern IntPtr CreateFontIndirect(
            [In, MarshalAs(UnmanagedType.LPStruct)]
            LOGFONT lplf   // characteristics
            );

      [DllImport("gdi32.dll")]
      public static extern bool DeleteObject(
            IntPtr handle
            );

      public static void Main()
      {
            LOGFONT lf = new LOGFONT();
            lf.lfHeight = 9;
            lf.lfFaceName = "Arial";
            IntPtr handle = CreateFontIndirect(lf);

            if (IntPtr.Zero == handle)
            {
                  Console.WriteLine("Can't creates a logical font.");
            }
            else
            {

                  if (IntPtr.Size == 4)
                        Console.WriteLine("{0:X}", handle.ToInt32());
                  else
                        Console.WriteLine("{0:X}", handle.ToInt64());         

                  // Delete the logical font created.
                  if (!DeleteObject(handle))
                       Console.WriteLine("Can't delete the logical font");
            }
      }
}

Output

C30A0AE5

In the preceding example, the CreateFontIndirect method is using a parameter of the type LOGFONT. The MarshalAs and In attributes are used to qualify the parameter. The program displays the numeric value returned by the method as a hexadecimal uppercase string.

Registering Callback Methods

To register a managed callback that calls an unmanaged function, declare a delegate with the same argument list and pass an instance of it via PInvoke. On the unmanaged side it will appear as a function pointer. For more information about PInvoke and callback.

For example, consider the following unmanaged function, MyFunction, which requires callback as one of the arguments:

typedef void (__stdcall *PFN_MYCALLBACK)();
int __stdcall MyFunction(PFN_ MYCALLBACK callback);

To call MyFunction from managed code, declare the delegate, attach DllImport to the function declaration, and optionally marshal any parameters or the return value:

public delegate void MyCallback();
[DllImport("MYDLL.DLL")]
public static extern void MyFunction(MyCallback callback);

Also, make sure the lifetime of the delegate instance covers the lifetime of the unmanaged code; otherwise, the delegate will not be available after it is garbage-collected.

@2016-02-15 20:04:24

Calling an interface method on a COM object (COM Interop)

COM Interop provides access to existing COM components without requiring that the original component be modified. When you want to incorporate COM code into a managed application, import the relevant COM types by using a COM Interop utility (TlbImp.exe) for that purpose. Once imported, the COM types are ready to use.

In addition, COM Interop allows COM developers to access managed objects as easily as they access other COM objects. Again, COM Interop provides a specialized utility (RegAsm.exe) that exports the managed types into a type library and registers the managed component as a traditional COM component.

At run time, the common language runtime (CLR) marshals data between COM objects and managed objects as needed.

C# uses .NET Framework facilities to perform COM Interop. C# has support for:

  • Creating COM objects.
  • Determining if a COM interface is implemented by an object.
  • Calling methods on COM interfaces.
  • Implementing objects and interfaces that can be called by COM clients.

The .NET Framework handles reference-counting issues with COM Interop so there is no need to call or implement AddRef and Release.

Creating a COM Class Wrapper

For C# code to reference COM objects and interfaces, you need to include a .NET Framework definition for the COM interfaces in your C# build. The easiest way to do this is to use TlbImp.exe (Type Library Importer), a command-line tool included in the .NET Framework SDK. TlbImp converts a COM type library into .NET Framework metadata — effectively creating a managed wrapper that can be called from any managed language. .NET Framework metadata created with TlbImp can be included in a C# build via the /R compiler option. If you are using the Visual Studio development environment, you only need to add a reference to the COM type library and the conversion is done for you automatically.

TlbImp performs the following conversions:

  • COM coclasses are converted to C# classes with a parameterless constructor.
  • COM structs are converted to C# structs with public fields.

A great way to check the output of TlbImp is to run the .NET Framework SDK command-line tool Ildasm.exe (Microsoft Intermediate Language Disassembler) to view the result of the conversion.

Although TlbImp is the preferred method for converting COM definitions to C#, it is not always possible to use it (for example, if there is no typelib for the COM definitions, or if TlbImp cannot handle the definitions in the typelib). In these cases, the alternative is to manually define the COM definitions in C# source code using C# attributes. Once you have created the C# source mapping, you simply compile the C# source code to produce the managed wrapper.

The main attributes you need to understand to perform COM mapping are:

  • ComImport - Marks a class as an externally implemented COM class.
  • Guid – Used to specify a universally unique identifier (UUID) for a class or an interface.
  • InterfaceType – specifies whether an interface derives from IUnknown or IDispatch.
  • PreserveSig – specifies whether the native return value should be converted from an HRESULT to a .NET Framework exception.

Declaring a COM coclass

COM coclasses are represented in C# as classes. These classes must have the ComImport attribute associated with them. The following restrictions apply to these classes:

  • The class must not inherit from any other class.
  • The class must implement no interfaces.
  • The class must also have a Guid attribute that sets the globally unique identifier (GUID) for the class.

The following example declares a coclass in C#:

// 
// declare FilgraphManager as a COM coclass 
// 
[ComImport, Guid("E436EBB3-524F-11CE-9F53-0020AF0BA770")] 
class FilgraphManager
{ 
}

The C# compiler will add a parameterless constructor that you can call to create an instance of the COM coclass.

Creating a COM Object

COM coclasses are represented in C# as classes with a parameterless constructor. Creating an instance of this class using the new operator is the C# equivalent of calling CoCreateInstance. Using the class defined above, it is simple to instantiate the class:

class MainClass 
{
    public static void Main() 
    {
        // 
        // Create an instance of a COM coclass - calls
        //
        // CoCreateInstance(E436EBB3-524F-11CE-9F53-0020AF0BA770, 
        //                  NULL, CLSCTX_ALL, 
        //                  IID_IUnknown, &f) 
        //
        // returns null on failure. 
        // 
        FilgraphManager f = new FilgraphManager(); 
    }
}

Declaring a COM Interface

COM interfaces are represented in C# as interfaces with ComImport and Guid attributes. They cannot include any interfaces in their base interface list, and they must declare the interface member functions in the order that the methods appear in the COM interface.

COM interfaces declared in C# must include declarations for all members of their base interfaces with the exception of members of IUnknown and IDispatch — the .NET Framework automatically adds these. COM interfaces which derive from IDispatch must be marked with the InterfaceType attribute.

When calling a COM interface method from C# code, the common language runtime must marshal the parameters and return values to/from the COM object. For every .NET Framework type, there is a default type that the common language runtime will use to marshal when marshaling across a COM call. For example, the default marshaling for C# string values is to the native type LPTSTR (pointer to TCHAR char buffer). You can override the default marshaling using the MarshalAs attribute in the C# declaration of the COM interface.

In COM, a common way to return success or failure is to return an HRESULT and have an out parameter marked as "retval" in MIDL for the real return value of the method. In C# (and the .NET Framework), the standard way to indicate an error has occurred is to throw an exception.

By default, the .NET Framework provides an automatic mapping between the two styles of exception handling for COM interface methods called by the .NET Framework.

  • The return value changes to the signature of the parameter marked retval (void if the method has no parameter marked as retval).
  • The parameter marked as retval is left off of the argument list of the method.

Any non-success return value will cause a System.COMException exception to be thrown.

This example shows a COM interface declared in MIDL (an IDL file which has a .idl file name extension) and the same interface declared in C# (note that the methods use the COM error-handling approach).

Here is the original MIDL version of the interface:

[ 
  odl, 
  uuid(56A868B1-0AD4-11CE-B03A-0020AF0BA770), 
  helpstring("IMediaControl interface"), 
  dual, 
  oleautomation 
] 
interface IMediaControl : IDispatch 
{ 
  [id(0x60020000)] 
  HRESULT Run(); 

  [id(0x60020001)] 
  HRESULT Pause(); 

  [id(0x60020002)] 
  HRESULT Stop(); 

  [id(0x60020003)] 
  HRESULT GetState( [in] long msTimeout, [out] long* pfs); 

  [id(0x60020004)] 
  HRESULT RenderFile([in] BSTR strFilename); 

  [id(0x60020005)] 
  HRESULT AddSourceFilter( [in] BSTR strFilename, [out] IDispatch** ppUnk);

  [id(0x60020006), propget] 
  HRESULT FilterCollection([out, retval] IDispatch** ppUnk); 

  [id(0x60020007), propget] 
  HRESULT RegFilterCollection([out, retval] IDispatch** ppUnk); 

  [id(0x60020008)] 
  HRESULT StopWhenReady(); 
};

Here is the C# equivalent of this interface:

using System.Runtime.InteropServices;

// Declare IMediaControl as a COM interface which 
// derives from the IDispatch interface. 
[Guid("56A868B1-0AD4-11CE-B03A-0020AF0BA770"),
    InterfaceType(ComInterfaceType.InterfaceIsDual)] 
interface IMediaControl // cannot list any base interfaces here 
{ 
    // Note that the members of IUnknown and Interface are NOT
    // listed here 
    //
    void Run();

    void Pause();
    
    void Stop();

    void GetState( [In] int msTimeout, [Out] out int pfs);

    void RenderFile(
    [In, MarshalAs(UnmanagedType.BStr)] string strFilename);

    void AddSourceFilter(
    [In, MarshalAs(UnmanagedType.BStr)] string strFilename, 
    [Out, MarshalAs(UnmanagedType.Interface)] out object ppUnk);

    [return : MarshalAs(UnmanagedType.Interface)]
    object FilterCollection();

    [return : MarshalAs(UnmanagedType.Interface)]
    object RegFilterCollection();
    
    void StopWhenReady(); 
}

Note how the C# interface has mapped the error-handling cases. If the COM method returns an error, an exception will be raised on the C# side.

To prevent the translation of HRESULTs to COMExceptions, attach the PreserveSig(true) attribute to the method in the C# declaration.

Using Casts Instead of QueryInterface

A C# coclass is not very useful until you can access an interface that it implements. In C++ you would navigate an object's interfaces using the QueryInterface method on the IUnknown interface. In C# you can do the same thing by explicitly casting the COM object to the desired COM interface. If the cast fails, then an invalid cast exception is thrown:

// Create an instance of a COM coclass:
FilgraphManager graphManager = new FilgraphManager();

// See if it supports the IMediaControl COM interface. 
// Note that this will throw a System.InvalidCastException if 
// the cast fails. This is equivalent to QueryInterface for 
// COM objects:
IMediaControl mc = (IMediaControl) graphManager;

// Now you call a method on a COM interface: 
mc.Run();
 

Putting It All Together

Here is a complete example that creates an AVI file viewer using C#. The program creates an instance of a COM coclass, casts it to a COM interface, and then calls methods on the COM interface.

// interop2.cs
using System;
using System.Runtime.InteropServices;
 
namespace QuartzTypeLib 
{
      // Declare IMediaControl as a COM interface which 
      // derives from IDispatch interface:
      [Guid("56A868B1-0AD4-11CE-B03A-0020AF0BA770"), 
      InterfaceType(ComInterfaceType.InterfaceIsDual)] 
      interface IMediaControl   // Cannot list any base interfaces here 
      { 
            // Note that IUnknown Interface members are NOT listed here:
 

            void Run(); 
            void Pause();
 
            void Stop();
 
            void GetState( [In] int msTimeout, [Out] out int pfs);
 
            void RenderFile(
                  [In, MarshalAs(UnmanagedType.BStr)] string strFilename);
 
            void AddSourceFilter( 
                  [In, MarshalAs(UnmanagedType.BStr)] string strFilename, 
                  [Out, MarshalAs(UnmanagedType.Interface)]
                  out object ppUnk);
 
            [return: MarshalAs(UnmanagedType.Interface)] 
            object FilterCollection();
 
            [return: MarshalAs(UnmanagedType.Interface)] 
            object RegFilterCollection();
            
            void StopWhenReady(); 
      }
      // Declare FilgraphManager as a COM coclass:
      [ComImport, Guid("E436EBB3-524F-11CE-9F53-0020AF0BA770")] 
      class FilgraphManager   // Cannot have a base class or
            // interface list here.
      { 
            // Cannot have any members here 
            // NOTE that the C# compiler will add a default constructor
            // for you (no parameters).
      }
}
 
class MainClass 
{ 
      /********************************************************** 
      Abstract: This method collects the file name of an AVI to 
      show then creates an instance of the Quartz COM object.
      To show the AVI, the program calls RenderFile and Run on 
      IMediaControl. Quartz uses its own thread and window to 
      display the AVI.The main thread blocks on a ReadLine until 
      the user presses ENTER.
            Input Parameters: the location of the AVI file it is 
            going to display
            Returns: void
      *************************************************************/ 
 
      public static void Main(string[] args) 
      { 
            // Check to see if the user passed in a filename:
            if (args.Length != 1) 
            { 
                  DisplayUsage();
                  return;
            } 
 
            if (args[0] == "/?") 
            { 
                  DisplayUsage(); 
                  return;
            }
 
            String filename = args[0]; 
 
            // Check to see if the file exists
            if (!System.IO.File.Exists(filename))
            {
                  Console.WriteLine("File " + filename + " not found.");
                  DisplayUsage();
                  return;
            }
 
            // Create instance of Quartz 
            // (Calls CoCreateInstance(E436EBB3-524F-11CE-9F53-0020AF0BA770, 
            //  NULL, CLSCTX_ALL, IID_IUnknown, 
            //  &graphManager).):
            try
            {
                  QuartzTypeLib.FilgraphManager graphManager =
                        new QuartzTypeLib.FilgraphManager();
 
                  // QueryInterface for the IMediaControl interface:
                  QuartzTypeLib.IMediaControl mc = 
                        (QuartzTypeLib.IMediaControl)graphManager;
 
                  // Call some methods on a COM interface.
                  // Pass in file to RenderFile method on COM object.
                  mc.RenderFile(filename);
        
                  // Show file. 
                  mc.Run();
            }
            catch(Exception ex)
            {
                  Console.WriteLine("Unexpected COM exception: " + ex.Message);
            }
            // Wait for completion. 
            Console.WriteLine("Press Enter to continue."); 
            Console.ReadLine();
      }
 
      private static void DisplayUsage() 
      { 
            // User did not provide enough parameters. 
            // Display usage. 
            Console.WriteLine("VideoPlayer: Plays AVI files."); 
            Console.WriteLine("Usage: VIDEOPLAYER.EXE filename"); 
            Console.WriteLine("where filename is the full path and");
            Console.WriteLine("file name of the AVI to display."); 
      } 
}
@2016-02-15 20:17:48

ComInterfaceType Enumeration

ComInterfaceType enumeration identifies how to expose an interface to COM.

Member name Description
InterfaceIsDual Indicates that the interface is exposed to COM as a dual interface, which enables both early and late binding. InterfaceIsDual is the default value.
InterfaceIsIUnknown Indicates that an interface is exposed to COM as an IUnknown -derived interface, which enables only early binding.
InterfaceIsIDispatch Indicates that an interface is exposed to COM as a dispinterface, which enables late binding only.

This enumeration works in conjunction with System.Runtime.InteropServices.InterfaceTypeAttribute.

By default, if an interface is not marked with the InterfaceTypeAttribute attribute, it is exposed as a dual interface.

An interface must be exposed as InterfaceIsIDispatch to allow Visual Basic 6 COM clients to receive event notifications.

@2016-02-16 08:14:40

The Composition of a COM DLL

All COM DLLs have the same internal composition regardless of which COM-aware language you build them in. The following figure illustrates the core atoms of the binary image of a COM DLL server:

A COM server contains some number of coclasses which is a type supporting at minimum the mandatory IUnknown interface. As shown in the above figure, the RawComServer.dll contains a single coclass named ComCar and it supports two interfaces: ICar and IUnknown.

COM servers also support a special sort of COM type termed a class factory (also termed a class object). COM class factories also support the mandatory IUnknown, as well as another standard interface named IClassFactory. This interface allows the COM client to create a given coclass in a language- and location-neutral manner. As you may be aware, it is possible for a COM class factory to support the IClassFactory2 interface (which derives from IClassFactory). The role of IClassFactory2 is to define additional methods to check for a valid license file before activating the object.

In addition to the set of coclasses and class factories, COM DLLs must support a small set of function exports. These function exports allow the COM runtime to interact with the internal types, as well as perform registration and unregistration of the COM binary itself. The following table provides a breakdown of each DLL export.

COM DLL Function Export

Meaning in Life

DllRegisterServer()

This method, which is technically optional, is used to install the necessary entries into the system registry.

DllUnregisterServer()

This method (also technically optional) removes any and all entries inserted by DllRegisterServer().

DllCanUnloadNow()

This method is called by the COM runtime to determine if the DLL can be unloaded from memory at the current time.

DllGetClassObject()

This method is used to retrieve a given IClassFactory interface to the COM client based on the CLSID of the COM class in question. Once this interface has been obtained, the client is able to create the associated coclass.

Comments

You must Sign In to comment on this topic.


© 2024 Digcode.com