Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# 4, COM interop and UPnP: A trying triumvirate

I'm trying to write a bit of code (just for home use) that uses UPnP for NAT traversal, using C# 4 and Microsoft's COM-based NAT traversal API (Hnetcfg.dll).

Unfortunately (or perhaps fortunately) the last time I had to do COM interop in .NET was sometime around the last ice age, and I seem to be fundamentally confused about C#'s use of dynamic types for interop and how to write a callback (so that the COM server calls my managed code).

Here's an exciting few lines of code:

// Referencing COM NATUPNPLib ("NATUPnP 1.0 Type Library")

using System;
using NATUPNPLib;

class NATUPnPExample
{
    public delegate void NewNumberOfEntriesDelegate(int lNewNumberOfEntries);

    public static void NewNumberOfEntries(int lNewNumberOfEntries)
    {
        Console.WriteLine("New number of entries: {0}", lNewNumberOfEntries);
    }

    public static void Main(string[] args)
    {
        UPnPNAT nat = new UPnPNAT();
        NewNumberOfEntriesDelegate numberOfEntriesCallback = NewNumberOfEntries;

        nat.NATEventManager.NumberOfEntriesCallback = numberOfEntriesCallback;

        nat.StaticPortMappingCollection.Add(4555, "TCP", 4555, "192.168.0.1", true, "UPnPNAT Test");

        // Presumably my NewNumberOfEntries() method should be called by the COM component about now

        nat.StaticPortMappingCollection.Remove(4555, "TCP");
    }
}

In the above code, the Add and Remove calls work absolutely fine. Terrific.

The trouble is, I would also like to know when the number of port mapping entries have changed, and to do so I need to register a callback interface (INATEventManager::put_NumberOfEntriesCallback), which must support the INATNumberOfEntriesCallback or IDispatch interfaces. VS2012's object browser describes INATEventManager::put_NumberOfEntriesCallback thusly:

dynamic NumberOfEntriesCallback { set; }

Right, so I was under the impression that in C# 4 I shouldn't have to decorate anything with fancy attributes and that I can register my callback simply by jamming a delegate into INATEventManager::put_NumberOfEntriesCallback in a vulgar manner and leaving .NET to worry about IDispatch and clear up the mess; but it appears that I'm terribly wrong.

So, er... What should I do to ensure my NewNumberOfEntries method is called?

I'm also slightly concerned that I can write nat.NATEventManager.NumberOfEntriesCallback = 1; or nat.NATEventManager.NumberOfEntriesCallback = "Sausages"; without an exception being thrown.

like image 813
Look Out Explosive Woofy Avatar asked Nov 06 '12 08:11

Look Out Explosive Woofy


2 Answers

It seems that I was able to make it work. Two options - with a custom interface "INATNumberOfEntriesCallback" (which does not seem to be declared in the type library btw, you need to declare it yourself) and using plain dispatch with DispId(0). The conversion to the IDispatch/IUnknown is preformed by the framework automatically. So:

Option 1.

Declare the INATNumberOfEntriesCallback and make a callback class which implements that interface (the tricky part is Guid - it comes from the "Natupnp.h" file, and does not seem to appear to be in the type library).

// declare INATNumberOfEntriesCallback interface 
[ComVisible(true)]
[Guid("C83A0A74-91EE-41B6-B67A-67E0F00BBD78")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface INATNumberOfEntriesCallback
{
    void NewNumberOfEntries(int val);
};

// implement callback object
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
public class CallbackNewNumberOfEntries : INATNumberOfEntriesCallback
{
    public void NewNumberOfEntries(int val)
    {
        Console.WriteLine("Number of entries changed: {0}", val);
    }
}

class NATUPnPExample
{
    public static void Main(string[] args)
    {
        var nat = new UPnPNAT();

        nat.NATEventManager.NumberOfEntriesCallback = new CallbackNewNumberOfEntries();

        nat.StaticPortMappingCollection.Add(4555, "TCP", 4555, "192.168.0.1", true, "UPnPNAT Test");

        // Presumably my NewNumberOfEntries() method should be called by the COM component about now

        nat.StaticPortMappingCollection.Remove(4555, "TCP");
    }
}

Option 2.

Use plain dispatch. The documentation says that you can use dispid(0) and it should be called, with 4 (!) parameters (see the remarks section in docs). So basically the following construction seems to work in "dispatch" way:

[ComVisible(true)]
[ClassInterface(ClassInterfaceType.AutoDispatch)]
public class CallbackDisp
{
    [DispId(0)]
    public void OnChange(string updateType, object obj, object name, object val)
    {
        Console.WriteLine("{0}: {1} = {2}", updateType, name, val);
    }
}

class NATUPnPExample
{
    public static void Main(string[] args)
    {
        var nat = new UPnPNAT();

        nat.NATEventManager.NumberOfEntriesCallback = new CallbackDisp();

        nat.StaticPortMappingCollection.Add(4555, "TCP", 4555, "192.168.0.1", true, "UPnPNAT Test");

        // Presumably my NewNumberOfEntries() method should be called by the COM component about now

        nat.StaticPortMappingCollection.Remove(4555, "TCP");
    }
}
like image 58
Nikolay Avatar answered Nov 11 '22 16:11

Nikolay


I had the same problem you had, and since there isn't much help on the topic your posting helped tremendously! It wouldn't let me comment on your answer because I don't have enough points or whatever but your answer is the best, but doesn't quite work how I thought it would.

nat.NATEventManager.ExternalIPAddressCallback = new CallbackDisp();

Works, using the same dispatch, and will tell you when the external IP changes. HOWEVER,

nat.NATEventManager.NumberOfEntriesCallback = new CallbackDisp();

only reports UPnP map changes from these conditions: A.) It was added/removed by the NATUPnP instance.. In this case:

    nat.StaticPortMappingCollection.Add();

OR B.) it was already mapped when the instance was created:

 var nat = new UPnPNAT();

As an example, if Utorrent was running when you started your program and you you had something to block the program from exiting(Console.WriteLine();) for example.. When you exit Utorrent the callback would fire, and notify you of the map changes. Which is exactly what I wanted in the first place. However, if you re-open Utorrent, or any other app that uses UPnP it will NOT fire the callback, and will not notify you of the change.

Needless to say it has been very frustrating. If you figure it out please share! I know I can easily implement the functionality by polling the StaticPortMappingCollection at a given interval, but it seems a little 'hacky' to me.

like image 25
Greg Avatar answered Nov 11 '22 17:11

Greg