Most seasoned C# programmers will be familiar with P/Invoke and the use of the [DllImport] attribute to call unmanaged functions from DLLs. There are plenty of cases (especially in Windows Forms) where the managed functionality is too limiting to get the desired outcome, so it is excellent to have a way of calling functions directly from the Windows API. (Of course, it would be even better if there were managed equivalents of that functionality, but we won’t get into that argument.)

Perhaps less common is the use of DllImport to call functions from custom/purpose-built unmanaged DLLs. The aim of this article is to demonstrate how to author a DLL in C/C++ whose functions can be called from managed code. Now, i’m certainly no stranger to C++, but thusfar in my career i’ve never had cause to write my own Dynamic Library. So, here’s what you need to know:

Project/Build Settings

When adding a new Win32 Project to a solution in Visual Studio, you’re presented with a wizard. Here, you need to specify the application type as DLL.

Architecture

It’s important to note that you can only call functions from a 32-bit DLL if your managed project targets the x86 platform, or if you are running a 32-bit OS and your managed project targets the Any CPU platform. If the latter is true, and you are running a 64-bit OS, you will get a BadImageFormatException when you try to call the unmanaged function. You have two options when selecting the architecture for your solution:

  • Set the platform for the unmanaged project to Win32, and the managed project to x86. Your code will run on both platforms.
  • Build both Win32 and x64 DLLs, import the function(s) from both and call the appropriate version according to the current platform/process. (Note: .NET 4 adds Environment.Is64BitProcess to aid in this, but earlier versions of the framework make this a lot harder to do.)

Debug Info

There’s no point generating a PDB file for the unmanaged DLL – ultimately you’re going to want to use the Release build to import the functions from.

Character Set

Strings in .NET are Unicode, and indeed the default character set for C/C++ projects now reflects this as well. When you write your import declaration, you will need to specify CharSet=CharSet.Unicode in the DllImport attribute. If you don’t do this, strings might be marshalled as ANSI instead.

Implementation

These sorts of DLLs (i.e. unmanaged, non-COM) lend themselves better to the functional programming paradigm than OO. You’re probably going to want to implement your DLL as a set of independent utility functions. Use standard Win32 data types wherever possible, bearing in mind that the marshaller in .NET supports the most common subset of types only. If you elect to write functions that take or return complex structures, be prepared to re-declare these in your managed project. The same applies to enumerations.

Module Definition (.def) File

When you’ve written the code for your unmanaged DLL, the next step is to declare which functions you want to export (i.e. make available to the managed project). To do this, you need to add a Module Definition file to your unmanaged project. Visual Studio will automatically hook this up to the linker configuration for you. The format of the file looks something like this:

MODULE "NameOfUnmanagedDLL"
EXPORTS
    Function1Name   @1
    Function2Name   @2

…and so on. Declaring the exported functions in this manner ensures that the DllImport attribute will be able to locate them. The alternative technique involves decorating the function prototypes with the __declspec(dllexport) keyword – but this will obfuscate the name of the function, so I wouldn’t advise it. Use a DEF file whenever you can.

Adding Product/Company Information

Since you will likely be distributing the unmanaged DLL with your application, it’s a good idea to add product/company/version information. This adds credibility and consistency to your binaries. To add this information, add a new Resource (.rc) file to the project, then add a Version resource. As with the Module Definition file, Visual Studio will automatically hook this up to the linker. Within the Version resource, you can specify descriptive information about the DLL.

Calling From Managed Code

Writing the Import Declaration

The first thing you want to do is re-write the signature of the function as declared in C/C++, adding the modifiers static extern in order to mark them as being located in an external DLL. Then, you need to replace the C/C++ data types (on the parameters and return value) with appropriate .NET types. In doing so, consider the following basic principles:

  • HANDLE, HWND and most other pointer types (excluding arrays and strings) resolve to IntPtr
  • Fixed-length arrays resolve to .NET arrays of the appropriate type
  • wchar_t *, char *, BSTR and LPCWSTR (as well as many other string types) resolve to String or, if the result is mutable, StringBuilder
  • DWORD resolves to UInt32 (but can be marshalled as Int32 if required by the calling code)
  • Any complex structure must be re-declared in the managed project. The marshaller will map the unmanaged structure onto the managed one.

A basic function such as:

DWORD CreateProcessMediumIL(WCHAR* path, WCHAR* cmdLine);

Could be imported as:

[DllImport("MediumElevation.dll", CharSet=CharSet.Unicode)]
public static extern int CreateProcessMediumIL(string path, string cmdLine);

Note that, in this example, the function returns a Win32 error code – by marshalling it as Int32 instead of UInt32, we can elegantly pass it to the constructor of Win32Exception.

Catching Possible Exceptions

When calling the method (formerly function), you should be prepared to handle DllNotFoundException and BadImageFormatException. If the function returns a Win32 error code or an HRESULT, you can use Win32Exception/Marshal.ThrowExceptionForHR() to throw an exception for the error.

The complete calling code for the aforementioned function might look like this:

try {
    int result = CreateProcessMediumIL(@"C:\Windows\System32\notepad.exe", null);
    if (result != 0) throw new Win32Exception(result);
}
catch (DllNotFoundException) {
    // the DLL was not in the current directory or any of the paths probed by LoadLibrary
}
catch (BadImageFormatException) {
    // the DLL is probably for a different platform (e.g. 32-bit DLL called from 64-bit)
}
catch (Win32Exception) {
    // something failed within the function itself
}

Final Words

There are a number of legitimate cases for calling functions from unmanaged DLLs, and there is also a subset of legitimate cases for developing purpose-built unmanaged DLLs to provide advanced functionality within your managed projects. Provided you follow these basic guidelines, you should be able to avoid most of the common pitfalls in achieving this.

Of course, there are some alternatives which may be more appropriate for your specific needs:

  • Create hybrid native/managed assemblies in C++ and reference these in your managed projects
  • Create COM components which encapsulate the required functionality, then use the RCW features to treat these like managed objects in code

However, if you only need a small number of independent functions, this technique will suit your needs most sufficiently.

Leave a reply

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> 

required