2004-10-05
| Table of Contents: |
| Rate This Article: | Add This Article To: |
( Page 4 of 4 )
Functions">
Defining Delegates for Callback Functions
By far, the most difficult problem I encountered during the translation was the issue of defining delegates for the many callback functions used by the cabinet interface. The first hurdle, that of creating a delegate that uses the Cdecl calling convention, appeared insurmountable and almost forced me to abandon the idea of creating an all-.NET interface in favor of using an unmanaged C wrapper. It took me several hours of searching message board archives before I ran across a message that described how to create such a delegate.
It turns out that it is possible to create a .NET delegate that uses the Cdecl calling convention. The trick is to change the calling convention on the internal Invoke method of the delegate. You can do that in the runtime, in IL assembly language, and in managed C++, but there's no direct way to do it in C#. If you want to create a Cdecl delegate in C#, you have to do the following:
- Define a placeholder attribute and apply it to the delegate that you want to modify.
- Compile the assembly.
- Disassemble the assembly to IL code using ILDASM.
- Edit the IL code and add the following line before the
Invokemethod of the delegate:modopt([mscorlib]System.Runtime.CompilerServices.CallConvCdecl) - Save and close the IL code file.
- Assemble the modified code using ILASM.
That sounds like a lot of work, especially when there are over 20 delegates to modify. Fortunately, the process is easily automated and can be added as a post-build step when compiling. It just requires that you define the placeholder attribute and create a program that can locate the attribute and apply the modopt code shown above.
Defining the Placeholder Attribute
The placeholder attribute is used to put metadata in the compiled assembly so that when it's disassembled it marks the delegates that need to be modified. It's about the simplest attribute that you can define:
[Serializable, AttributeUsage (AttributeTargets.Delegate)]
public sealed class CallConvCdeclAttribute : Attribute
{
public CallConvCdeclAttribute()
{
}
}
With that attribute defined, you can then apply it to the delegates that need to be modified. For example, here's the FDI memory allocation delegate:
[CallConvCdecl]public delegate IntPtr FdiMemAllocDelegate(int cb);
When compiled and then disassembled, the IL code for the delegate looks like this:
.class public auto ansi sealed FdiMemAllocDelegate
extends [mscorlib]System.MulticastDelegate
{
.custom instance void CabDotNet.CallConvCdeclAttribute::.ctor() = ( 01 00 00 00 )
.method public hidebysig specialname rtspecialname
instance void .ctor(object 'object',
native int 'method') runtime managed
{
} // end of method FdiMemAllocDelegate::.ctor
.method public hidebysig virtual instance native int
Invoke(int32 cb) runtime managed
{
} // end of method FdiMemAllocDelegate::Invoke
.method public hidebysig newslot virtual
instance class [mscorlib]System.IAsyncResult
BeginInvoke(int32 cb,
class [mscorlib]System.AsyncCallback callback,
object 'object') runtime managed
{
} // end of method FdiMemAllocDelegate::BeginInvoke
.method public hidebysig newslot virtual
instance native int EndInvoke(class [mscorlib]System.IAsyncResult result)
runtime managed
{
} // end of method FdiMemAllocDelegate::EndInvoke
} // end of class FdiMemAllocDelegate
I've highlighted the code that applies the CallConvCdeclAttribute attribute to the delegate, and the Invoke method that needs to be modified. The new Invoke method with the prescribed modification looks like this:
.method public hidebysig virtual instance native int
modopt([mscorlib]System.Runtime.CompilerServices.CallConvCdecl)
Invoke(int32 cb) runtime managed
{
} // end of method FdiMemAllocDelegate::Invoke
Automating the Changes
Making those changes by hand is repetitive and tedious, especially if you have to do it every time you recompile the assembly. But computers are good at repetitive and tedious tasks, and it was very easy to create a C# program that locates the marked delegates and applies the required changes. The program I wrote, callconvhack.cs, is a direct translation of the Perl program that I found in the online forum discussion. The program is included in the listings archive for this article.
The final piece of the puzzle is adding a post-build step in Visual Studio so that these changes are made every time you build the program. To add a post build step, right click on the project in Solution Explorer and select Properties from the pop-up menu. In the project's Property Pages dialog, select the Build Events item under Common Properties in the tree view and edit the Post Build Event Command Line property. Here are the lines that I included there:
ildasm "$(TargetPath)" /out:"$(TargetDir)\$(TargetName).il1" "..\..\callconvhack" "$(TargetDir)\$(TargetName).il1" "$(TargetDir)\$(TargetName).il2" ilasm /DLL "$(TargetDir)\$(TargetName).il2"
For this to work, you'll probably have to modify your PATH environment variable so that the .NET Framework tools ILDASM.EXE and ILASM.EXE can be found. Also, I placed the callconvhack program in the project's root directory. You might want to place it somewhere else.
Marshaling Character Buffers
The file read and write delegates need to read from or write to an unmanaged array of characters that is supplied by the caller (the cabinet functions). The C prototype for a suitable write function, for example, looks like this:
unsigned int file_write(int hf, void FAR *pv, unsigned int cb);
The pv parameter points to a character buffer whose size is defined in the cb parameter. In C#, we have to convert that unmanaged character buffer to an array of bytes that has the proper size so that the C# method looks like this:
int FileWrite(IntPtr hf, byte[] buffer, int cb)
As you might expect, we use the MarshalAs attribute again, this time applying it to the second parameter. The delegate definition becomes:
public delegate Int32 FdiFileReadDelegate(IntPtr hf,
[In, Out][MarshalAs(UnmanagedType.LPArray,
SizeParamIndex=2, ArraySubType=UnmanagedType.U1)] byte[] buffer,
int cb);
The first parameter in the MarshalAs attribute constructor tells the marshaler that the unmanaged parameter is an array pointer. The SizeParamIndex property tells the marshaler that the number of elements in the array can be found in the third method parameter. SizeParamIndex uses zero-based indexing. Finally, the ArraySubType property defines the size of the array elements, in this case an unsigned one-byte value. The [In, Out] statement tells the marshaler that this array must be marshaled both ways. When the delegate is called at runtime, the marshaler creates a managed array of bytes from the memory referenced by the pointer supplied
by the caller. When the method returns, the marshaler copies the bytes from the buffer back to the referenced memory location. The file write delegate is very similar except that it only needs to be marshaled on call, not on return. As a result, the [In, Out] statement is replaced with [In].
Creating Managed Prototypes for Cabinet DLL Functions
After the trouble I went through translating structures and defining delegates, creating managed prototypes for the unmanaged DLL functions turned out to be absurdly easy. All I had to do was translate the Windows types to .NET types and apply the DllImport attribute to each of the methods. For example, the CABINET.DLL function FDICreate has this C prototype:
BOOL FDICopy(HFDI hfdi,
char FAR *pszCabinet,
char FAR *pszCabPath,
int flags,
PFNFDINOTIFY pfnfdin,
PFNFDIDECRYPT pfnfdid,
void FAR *pvUser);
The corresponding managed prototype in C# is:
[DllImport("cabinet.dll",
CallingConvention=CallingConvention.Cdecl,
EntryPoint="FDICopy")]
private static extern bool FdiCopy(
IntPtr hfdi,
string cabinetName,
string cabinetPath,
int flags,
FdiNotifyDelegate fnNotify,
FdiDecryptDelegate fnDecrypt,
IntPtr pvUser);
The DllImport attribute tells the runtime where to find the function, the calling convention that it uses, and the real name of the function in the DLL.
I did make one change to the API. The FDICreate function in the DLL takes a parameter that identifies the CPU type as being an 80286, an 80386, or unknown. This parameter is included for backward compatibility with the 16-bit version of CABINET.DLL and is ignored in the 32-bit versions. I figured there was no need to have an unused parameter in the API, so I made the managed prototype to FDICreate private, and created my own FdiCreate method that doesn't require that parameter. The public FdiCreate method calls the managed prototype, supplying the legacy CPU type parameter.
I also marked the FDICopy managed prototype as private and made a managed FdiCopy method that simplifies calling the cab file decompressor. The FDICopy function in CABINET.DLL expects the cabinetPath parameter to contain a trailing backslash (directory separator character). This is quite inconvenient in .NET, because the Path.GetDirectoryName method doesn't include that character. Considering how often I ran into that problem when developing this interface assembly, I figured I'd save everybody else the frustration. The managed FdiCopy method ensures that the directory separator character is included where required.
I also added code to create a GCHandle for the userData
parameter so that the managed type can be passed unmolested through the cabinet interface. This frees client programs from having to manage this, and possibly forgetting to deallocate the GCHandle, resulting in a memory leak. You'll see more about using GCHandle in the next article.
End of Translation
Creating this .NET interface to CABINET.DLL turned out to be much more work than I had expected. In the process, I learned a lot about interop marshaling, and more than I thought I'd ever need to know about delegates and Microsoft Intermediate Language (MSIL). I also found some very worthwhile sources of in-depth information about the .NET runtime, both in the .NET SDK documentation and online.
In my next article, I'll show how to use this interface assembly to create programs that create and read cabinet files. It turns out that creating the interface isn't the only place where I had to worry about converting between managed and unmanaged types. Stay tuned!
You can find the code and project files for this article here.
![]() |
|


