Logo: C# Computing
Web CsharpComputing.com

C# Tutorial Lesson 18: Calling unmanaged dlls from C#

Let's say you would like to play a Wav file from .Net. How would we do it? One way to achieve this is to use Platform Invocation services (PInvoke).

//code starts
using System;
using System.Runtime.InteropServices;
class Test{
    public static extern bool PlaySound(string filename,long hmodule, int dword );
    public static void Main()
        bool result;
} //code ends

Why did I use 0x0001|0x00020000?  In order to call PlaySound, we need to pass certain bitwise parameters. I would like to play sound from a file, which corresponds to SND_FILENAME flag and play it asyncronioulsy which corresponds to SND_ASYNC flag. DotNet does not have header files and it is our responsibility to locate relevant constants in the header files and declare them in C#. I have located octal values for SND_FILENAME| SND_ASYNC in Mmsystem.h.

Compile and run this program. Do you notice a startup delay? Does your computer runs slower?

Let's compare PlaySound performance in a managed and an unmanaged application. To get distinct results, we will select a fairly large, 50 MB, sound file and play it monitor applications through task manager.

Notice how long it takes to load 01.wav into memory and how much memory your program takes when it runs. Now, do the same with the program above. What do you find?

It takes .Net twice as much memory to play the same sound file as a direct API call. Let's disassemble this code with IlDasm to see what happens behind the scenes. Here is a really cool MSIL code for it.

.assembly hello{}
.class Test{ //pinvokeimpl marks a method that uses platform invocation services. Such methods may not have a body. All pinvokeimpl //methods are static. winapi uses native api convention
    .method public static pinvokeimpl("winmm.dll" winapi) 
    bool PlaySound(string filename,int64 hmodule, int32 dword) 

     .method public static void Main() cil managed
    .maxstack 3
    .locals (bool V_0)
     ldstr "01.wav"
     ldc.i4.0 //push zero into the stack as int32. this integer is now on top of the stack
     conv.i8//we need int64 so we call conv.i8 to convert the integer on top of the stack to int64.
    ldc.i4 0x20001
    call bool Test::PlaySound(string,int64,int32)
    call void [mscorlib]System.Console::WriteLine(bool)
    } // end of method Test::Main


The code in VB.Net looks almost the same

imports System
imports System.Runtime.InteropServices
class Test
public declare function PlaySound lib "winmm.dll"(filename as string, hmodule as long, dword as integer ) as boolean
public shared Sub Main() 

dim result as boolean
End Sub
End Class

There is one unpleasant surprise: You have to translate your octal numbers to a decimal or hexadecimal format because VB does not have octal numbers. To avoid translation, I set the last integer to 0. 

Conclusion: There are a number of issues that make the use of PInvoke difficult

  1. Compiler may mangle method's name so that to make your code to work you need to dump your binary file and find how  the method that you plan to call is actually named. You do not have to do it for Win32 APIs, but for all other APIs you will need to specify mangled names for all your pinvoked methods.
  2. The language that you are using may have a different notion of a type than the type declared in dll
  3. .Net framework is doing a lot of work behind the scenes which may lead to double the memory use for a given dll call
  4. .Net has no header files. When it comes to passing flags or predefined structures, you have to look up their values in corresponding header files and then pass those values manually to each method that requires them.

Having mentioned the difficulties, it is important to stress that the runtime takes care of all low level details to access an unmanaged dll. From the point of view of the programmer, there is often little difference between calling a managed and an unmanaged dll