Last Updated: 6/2003

Topics

 

Win32API Interoperability (DllImport)
Inside the class declare the function using the [DllImport("dllFilename")] prototype syntax. To use the DllImport attribute you must first include a reference to System.Runtime.InteropServices. The imported function needs to be declared both static and extern.

For example:

using System;
using System.Runtime.InteropServices;

class myClass {
    [DllImport("winmm.dll")]
    private static extern long sndPlaySoundA(string lpszFile, int uFlags);

    public static void Main() {sndPlaySoundA("somefile.wav", 0);}
}

  • Default installation path for the C/C++ header files is:
        \Program Files\Microsoft Visual Studio .NET\vc7\PlatformSDK\Include
     
  • The {In], [Out] and [MarshallAs] attributes can be applied to each parameter in the imported function declaration to find tune the conversion process. Where a buffer has to be passed (e.g. a string of a specific length to receive output that will be written to by the called function) it is not possible to specify that length through attributes - it is necessary to allocate the buffer in C#, then pass it. The GetWindowText example later on shows this in more detail.
     
    C/C++ MarshallAs UnmanagedType.value
    HWND IntPtr (IntPtr.Zero for null)
    LPCOLESTRLPWStr
    LPTSTRLPTStr (T's width is platform dependent unless overridden by DllImportAttribute's CharSet parameter).
    This is the default marshalling for a C# string
    lp Interface (e.g. punkToolbarSite)IUnknown
    lp Structure (e.g. lpStruct)LPStruct

  • Example: Setting a window to 'Always on top'
    [DllImport("user32.dll", CharSet=CharSet.Auto)]
    public static extern bool SetWindowPos(
        IntPtr hWnd, IntPtr hWndInsertAfter,
        int
    x, int y, int cx, int cy, uint uFlags);
    ...
    SetWindowPos(hWnd, (IntPtr) HWND_TOPMOST,
        0, 0, 0, 0,SWP_NOMOVE | SWP_NOSIZE);
     
  • Example: Reading title from a window caption'
    [DllImport("user32.dll", CharSet=CharSet.Auto)]
    public
    static extern int GetWindowText(
        IntPtr hWnd,
       
    [Out, MarshalAs(UnmanagedType.LPTStr)] string lpString,
       
    int nMaxCount
    );
    ...
    string
    titleText = "".PadLeft(100, ' ');
    GetWindowText(hWnd, titleText, 100);

Back to Topic List 

 

Win32API Interoperability (Callbacks)
To use a Win32API function that requires a callback (such as EnumDesktopWindows)  declare a delegate for the callback, then reference that delegate in the DllImport marked prototype.
  • Example:
C++ Definitions
// The API Function
BOOL EnumDesktopWindows(
  HDESK hDesktop,    // Handle to desktop to enumerate
  WNDENUMPROC lpfn// Callback function
  LPARAM lParam      // Value to pass to callback function
);

// The callback required by the API function
BOOL CALLBACK EnumWindowsProc(
    HWND hwnd,    // Handle to parent window
   
LPARAM lParam // Application-defined value
);

C# Code
[DllImport("user32.dll", CharSet=CharSet.Auto)]
public static extern bool EnumDesktopWindows(
    IntPtr hDesktop, // Handle to desktop to enumerate
   
MyCallback lpfn, // Callback function
   
int lParam       // Value to pass to callback function
);

// The prototype for the callback
public
delegate bool MyCallback(IntPtr hWnd, int lParam);

// The implementation of the callback
public static bool theCallback(IntPtr hWnd, int lParam) {
    string titleText = "".PadLeft(100, ' ');
    GetWindowText(hWnd, titleText, 100);
    Console.WriteLine("{0} {1}", hWnd, titleText);
    return true;
}

...
// Calling of the function
MyCallback code = new MyCallback(theCallback);
EnumDesktopWindows(IntPtr.Zero, code, 0);
...

Back to Topic List 

 

Instantiating COM objects (CoCreateInstance)
COM Wrapper classes are used instead of calling CoCreateClass directly. For each COM class you wish to import, a wrapper is produced then instead of calling CoCreateClass we simply 'new' the COM Wrapper class and let .NET handle the rest.

A COM Wrapper class consists of a non-inherited, interface-free class marked with the appropriate attributes:

  • [ComImport]
    Marks the class as being a COM object. This attribute can also be placed on an interface definition to mark that interface as being a COM Interface
     
  • [Guid("clsid")]
    Associates a COM class ID (CLSID) with the class. This attribute can also be placed on an interface definition to associate the COM interface ID (IID) to it.

Example:

[ComImport, Guid("00BB2763-6A77-11D0-A535-00C04FD7D062")]
public class CLSID_AutoComplete {}

[ComImport, Guid("03C036F1-A186-11D0-824A-00AA005B4383")]
public class CLSID_ACListISF {}

...
// Following line is then equivalent to
CLSID_AutoComplete ac = new CLSID_AutoComplete();
...

All this presumes you don't have a TypeLib for the COM object, else you would just let VS import it and use the resulting COM Wrappers in the InterOp.ImportedDll directly.

Note:
If the GUID is expressed in the header file as

    DEFINE_GUID(CLSID_AutoComplete, 0x00BB2763L, 0x6A77, 0x11D0,
        0xA5, 0x35, 0x00, 0xC0, 0x4F, 0xD7, 0xD0, 0x62);

 this format needs to be converted to the single string format shown above. This is simply a case of merging the number into blocks of the correct length (not forgetting to remove the 0x hex prefix from all numbers and the L long postfix from the first number.

Back to Topic List 

 

COM Interfaces on COM Wrappers
('QueryInterface')

See above section for details on the [ComImport] and [Guid] attributes. These are used to tell the compiler these are COM interfaces and what IID is associated with the interface.

The [InterfaceType] attribute tells the compiler whether it should use IDispatch or IUnknown, or if it can use both (Dual). Since IDispatch is really late binding and requires additional support, I tend to specify IUnknown. The default is dual.

Again, interfaces can be automatically created from the TypeLib if one is available.

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("EAC04BC0-3791-11d2-BB95-0060977B464C")]
public interface IAutoComplete2 {

// virtual HRESULT STDMETHODCALLTYPE Init(
//    [in] HWND hwndEdit,
//    [unique][in]   IUnknown *punkACL,
//    [unique][in] LPCOLESTR pwszRegKeyPath,
//    [in] LPCOLESTR pwszQuickComplete) = 0;
void Init(
    [In]System.IntPtr phwnd,,
   
[In, MarshalAs(UnmanagedType.IUnknown)] object punkACL,
    [In, MarshalAs(UnmanagedType.LPWStr)] string pwszRegKeyPath,
    [In, MarshalAs(UnmanagedType.LPWStr)] string pwszQuckComplete);

// virtual HRESULT STDMETHODCALLTYPE Enable(
//    /* [in] */ BOOL fEnable) = 0;
void Enable ([In] bool fEnable);

// virtual HRESULT STDMETHODCALLTYPE SetOptions(
//    [in] DWORD dwFlag) = 0;
void SetOptions([In, MarshalAs(UnmanagedType.I4)] int dwFlag);

// virtual HRESULT STDMETHODCALLTYPE GetOptions(
//    [out] DWORD *pdwFlag) = 0;

void
GetOptions([Out, MarshalAs(UnmanagedType.I4)] out int pdwFlag);

}

Obtaining an Interface is then simple: simply case the COM Wrapper to the interface!

// Casting the C# class is the equivalent of calling IUnknown->QueryInterface on that class
IAutoComplete2 ac = (IAutoComplete2) new CLSID_AutoComplete();

Unfortunately, it is not currently possible to inherit COM interfaces. If interface 2 includes all the items in interface 1, those items have to be repeated manually in interface 2: you can't just 'public class interface2 : interface1' like you can with normal C# interfaces.


Back to Topic List 

 

[PerserveSig]

In COM, a function typically returns a HRESULT indicating success / failure, with the actual return value from the function being returned in a by-reference parameter. For example:

HRESULT AddTwoNumbers([in] x, [in] y, [out, retval] xPlusY)

While in C# we would typically use the result of the function as the result and throw an exception to indicate failure. For example:

int AddTwoNumbers(int x, int y) {
    return x+y;  // Could cause overflow exception
}

.NET does the conversion automatically between the two formats (which is why all the functions in the IAutoComplete2 interface example above return void). Placing [PreserveSig] on the function prototype will prevent this conversion. This attribute can also be used inside the DLLImport attribute, but since the sorts of functions imported by DDLImport rarely use COM, the default is here is not to perform the conversion.


Back to Topic List 

 

Auto Complete Example
// Casting the C# class is the equivalent of calling IUnknown->QueryInterface on that class
IAutoComplete2 ac = (IAutoComplete2) new CLSID_AutoComplete();
ac.SetOptions((int)(ACO.AUTOSUGGEST | ACO.ACF_UPDOWNKEYDROPSLIST));
// ac.Init(this.textBox1.Handle, new CLSID_ACListISF(), null, null);
ac.Init(this.textBox1.Handle, new myEnum(), null, null);

 

Back to Topic List 

 

The Manifest

The Manifest (see earlier notes) contains:

  • The identity of the assembly which comprises of the name, version and "culture" (culture = new term for localisation ID)
  • A list of all the names of all the files that make up the assembly (note: all files for a particular assembly have to exist in the same folder as the manifest - sub folder divisions are not allowed)
  • A hash checksum for the assembly generated from all files (i.e. you can't change a single file in the assembly without invalidating that assembly)
  • A list of other assemblies (and there hash checksum) that this assembly requires / wants
  • Security permissions for access to the assembly
  • Details of all the types/classes defined in the assembly
  • The manifest can be examined via the ildasm tool

Not all data types are supported by all .NET languages. For example, if you create a unsigned long public property in C# that class cannot be used by VB.NET as it has no equivalent.

Back to Topic List