Wednesday, July 11, 2007

Using Glib Callbacks with Managed Delegates on Windows

After wrestling with GStreamer for a week on Windows, I was delighted and disgusted to learn that the solution to my problem involved a simple string manipulation. My grin was mighty toothy. Sharp teeth.

I then had a new problem: Native Banshee code needs to call managed Banshee code every now and again (every 200 milliseconds) while Gstreamer is GStreaming. These callbacks are handled on the unmanaged side with glib mechanisms such as g_timeout_add. The problem is, glib uses the cdecl calling convention. If you try to set up a callback to managed delegates, which do not use the cdecl convention on Windows, you'll hit a nasty error when you call a delegate. Specifically, you'll encounter my good friend, the every-cryptic, "An unhandled exception of type 'System.AccessViolationException' occurred in gtk-sharp.dll".

The solution (thanks Miguel) is round-about, but with some jiggering in Visual Studio 2005 you can Set it and Forget it©. Here's the step-by-step:
  • Download and install ActivePerl for Windows.
  • Grab gtk-sharp/gapi-cdecl-insert. I just put it in C:\
  • You may need to edit gapi-cdecl-insert depending on whether or not I've committed these changes to SVN by the time you read this. Look at line 36. If it looks like this:

    `ildasm $assembly /out:$basename.raw`;

    Then you need to change it to this:

    `ildasm "$assembly" /out:"$basename.raw"`;

    (Add quotes to $assembly and $basename.raw). Do the same for line 49 by putting quotes around $, if they aren't already there.
  • Specify the Glib.CDeclCallback attribute for each delegate definition you will be calling with glib. For example:

    internal delegate void GstPlaybackEosCallback(IntPtr engine);

  • Open the Properties for the C# library project with the delegates of interest.
  • Open the Build Events tab.
  • Click the Edit Post-build button.
  • First add the following:

    Call "C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin\sdkvars.bat"

    Obviously, adjust the install path for Visual Studio as necessary (there is no sufficient macro; grrr).
  • On the next line, add this:

    Call C:\Perl\bin\perl.exe C:\gapi-cdecl-insert "$(TargetPath)"

    Again, adjust your Perl and gapi-cdecl-insert paths as appropriate.
  • For whatever reason, VS fucks up the Perl script at the end, so you need to add one final line to the post-build events:

    Call ilasm /DLL /QUIET /DEBUG "$(TargetDir)\$(TargetName).il"

    Remove the /DEBUG flag when you build for release.
Now try not to have too much fun fixing your glib-facilitated unmanaged-to-managed callback functions on Windows!

Great and terrible news! None of the above craziness is necessary. Thanks to Jonathan Pryor who points out in the comments that all you need to do is specify the [UnmanagedFunctionPointer(CallingConvention.Cdecl)] attribute for the delegate definitions. No Perl necessary! This only works for the 2.0 framework.


Jonathan Pryor said...

You missed the much easier, but .NET 2.0 API specific approach: add a UnmanagedFunctionPointer attribute to the delegate:

[UnmanagedFunctionPointer (CallingConvention.Cdecl)]
public delegate void SignalHandler (int signal);

This lets the runtime know that the delegate/function pointer refers to a function with CDECL calling convention.

If you need .NET 1.0 compatibility, this won't work, but it should work as expected under .NET 2.0 and Mono (supported added in 2005 --

- Jon

Jb Evain said...

Man, why do you bother using PERL when you could write that patcher with Cecil in a few lines ;)

Scott said...

Jonathan, where were you /before/ I spent half a day on this? ;)

JB, if the coding guidelines allowed, I'd write all my code using only Cecil! ;p