Monday, March 31, 2008

Bonjour for .Net

In implementing Mojo for Windows, one of our first tasks was to get Bonjour working. We looked for a good solution to this problem for months with no answer. And then, one fine day in July, we discovered this:
http://craz.net/programs/ZeroconfNetServices/

A most generous soul named David Hammerton had created a "wrapper around Apple's mDNS Windows library (dnssd.dll) which provides two .NET classes, very similar to Cocoa's NSNetService and NSNetServiceBrowser". Since we were already familiar with Cocoa's classes, this was even easier for us.

And after many months of working with the library, and slowly testing it, and improving it, we're happily releasing an updated version of it today!

We've added several things, such as the ability to update TXT records after publishing, and the ability to monitor TXT records for updates. We also improved the speed of resolving a service, fixed a few bugs, and added some minor improvements. (The download also contains a detailed change log.) The result is a library that's almost a replica of what's been available on every Mac since 2002. :)

Download it HERE

The download comes as a zipped visual studio solution. Inside are 3 projects. 1 is the actual Zeroconf dll library. The other is a sample app to publish a service, and also includes code to later update the TXT record. The third is a sample app to browse for services, resolve services, and monitor TXT records for updates.

The solution will open just fine using the free Visual C# express. Also, I've left the compiled versions in their respective build\Release folders just in case you don't feel like compiling.

To use these, you will need to install Bonjour for Windows.

These changes have also been reported to the original author. If you have questions regarding the code, you can contact either the original author, or us.

PS - for those who don't know, Zeroconf (zero configuration networking) is a technology that allows you to automatically discover services on a local network. It's what iTunes uses to automatically find other iTunes shares. Apple's named it's particular implementation Bonjour.

-- UPDATE --
The code now has it's own Google Code Project. Check it for the latest updates.

-- UPDATE 2 --
The project now has it's own Google Groups Mailing List. Please use the mailing list for questions, comments and general troubleshooting.

 

46 comments:

Chris said...

Can you say what kind of license this code falls under?

Robbie Hanson said...

The original code was released under public domain. This is also the case for this code. You're free to do whatever you want with it.

Chris said...

Great, thanks a bunch! Just today I was working on the same thing. But if this does the job, I shouldn't re-invent it.

Manty said...

Nice work, just what I was looking for. Many thanks!

action said...

not sure if im just being stupid or not,... this works great, but how do i get the socket connection so i can actually send data over the connection?

Robbie Hanson said...

Hi action,

You're not being stupid, it's an honest question and I can see why you're asking it. Bonjour will only tell you the IP:port of the service. Sometimes this is all people need. Other times people want to connect - but of course the way they connect will depend on the protocol (eg. TCP, UDP, etc). In other words, Bonjour doesn't create the socket for you, it's just a way to discover services.

But don't worry, it's really easy to connect once you know the address. In the DidResolveService callback, just use the service.Addresses property to get a list of System.Net.IPEndPoint. There may be multiple items in here, including IPv4 and IPv6. Then just use the IPEndPoint of your choice to connect in whatever manner is appropriate for your service.

If you don't support IPv6, you can check for AddressFamily == AddressFamily.InterNetwork.

action said...

thanks for the quick response, i'll give this a shot!

Anonymous said...

Thank you, absolutely great work! I've been using the original library the last few days and it has some strange bugs in it which caused me some headaches. But your improved version works like a charm, very fast! Thanks :-)

Anonymous said...

Thanks a mint! Saved me a lot of time

brian dunnington said...

what do you recommend as the best way to detect if bonjour/mDNSresponder is installed/running on the machine? in our app, the bonjour discovery is an 'extra' feature that is not required/necessary, so we dont want to fail if it is not available, but use it if it is.

so far, i have been using code like the code below, but it seems to result in sporadic CallbackOnCollectedDelegate MDA warnings being generated (i am assuming because i stop/destroy the NetServiceBrowser before the underlying unmanaged code makes the callbacks when bonjour is enabled):

try
{
NetServiceBrowser nsb = new NetServiceBrowser();
nsb.SearchForBrowseableDomains();
nsb.Stop();
nsb = null;
isSupported = true;
}
catch
{
Console.WriteLine("Bonjour is not supported");
isSupported = false;
}

Robbie Hanson said...

Hi Brian,

It's been my experience that an exception would be thrown the very first time any method in mDNSImports with "[DllImport("dnssd.dll")]" is called. In the case of the code you posted, I would have expected it to barf on the call to "mDNSImports.DNSServiceEnumerateDomains", which happens in "SearchForBrowseableDomains".

Another possibility is to simply search for the dnssd.dll within the system folder to detect if bonjour is installed. I believe this is also the recommended way of detecting bonjour via installers.

brian dunnington said...

you are right - when Bonjour is *not* installed, it fails right away, the exception is caught, and the code works as intended.

the problem is when Bonjour *is* available. at that point, the dnssd.dll is found and the SearchForBrowsableDomains methos completes successfully. however, i dont really want to search for any domains (at this point - it is just a detection feature), so when i .Stop() the browser and/or set it to null, the internal delegates go out of scope and that is when the runtime complains.

i think i will try just looking for the dnssd.dll, but i wasnt sure if it could be in varying locations. alternatively, i have implemented another dnssd.dll method (DNSServiceGetProperty) that simply tries to return the version of the service. it still invokes the dll, so i can check for failure, but avoids any delegate dependencies.

Robbie Hanson said...

Ah, I see. Sorry I misunderstood. I will take a look at the bug and try to fix it soon.

"i have implemented another dnssd.dll method (DNSServiceGetProperty) that simply tries to return the version of the service."

Let me know if you'd be willing to submit this, as I'd be interested in adding this method to the project. Also, I'd be happy to add you as a developer to the project so you'd be able so commit such improvements and fixes.

brian dunnington said...

well, i wouldnt say i am much of a interop guru, so i struggled getting the parameters just right, but here is what i came up with. it works, but i wouldnt plop it in there as-is until someone with more knowledge than i took a look at it:

in mDNSImports:

[DllImport("dnssd.dll")]
public static extern DNSServiceErrorType DNSServiceGetProperty(
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(UTF8Marshaler))]String name,
ref IntPtr result,
ref UInt32 size);

then in DNSService (or where ever):

public int GetVersion()
{
IntPtr result = IntPtr.Zero;
try
{
int version = 0;
int size = Marshal.SizeOf(typeof(UInt32));
result = Marshal.AllocCoTaskMem(size);
UInt32 rsize = (UInt32)size;
DNSServiceErrorType error = mDNSImports.DNSServiceGetProperty(ZeroconfService.DNSServiceProperty.DaemonVersion, ref result, ref rsize);
if (error != DNSServiceErrorType.kDNSServiceErr_NoError)
throw new DNSServiceException("DNSServiceGetProperty", error);
version = result.ToInt32();
return version;
}
finally
{
if (result != IntPtr.Zero) Marshal.FreeCoTaskMem(result);
}
}

there is more to do (the int returned from the function can be parsed apart to determine the major/minor version, etc, but it is a start.

Andrew said...

Outstanding work guys! This has just made my work so much easier. Having started to struggle with Apple's antique managed C++ wrapper, I then came across your library, which works like a charm. Thanks a lot for releasing this.

Michael Rubinstein said...

Thanks for the good work.

I've been using the code from a WPF application. I created a "glue" class to attach the asynch callbacks to the GUI thread.

The class:

public class DispatcherPatcher : System.ComponentModel.ISynchronizeInvoke
{
Dispatcher dispatcher;

public DispatcherPatcher(Dispatcher dispatcher_)
{
dispatcher = dispatcher_;
}

#region ISynchronizeInvoke Members

public IAsyncResult BeginInvoke(Delegate method, object[] args)
{
throw new NotImplementedException();
}

public object EndInvoke(IAsyncResult result)
{
throw new NotImplementedException();
}

public object Invoke(Delegate method, object[] args)
{
return dispatcher.Invoke(method, args);
}

public bool InvokeRequired
{
get { throw new NotImplementedException(); }
}

#endregion
}

To use it:

nsBrowser.InvokeableObject = new DispatcherPatcher(this.Dispatcher);

Everything seems to work fine.

Servant said...

It works! Awesome - thanks.

One issue - I want to use this in a product for which Apple's license on the Bonjour executable isn't practical (it's an embedded app). If I uninstall Bonjour, but leave dnssd.dll on the machine or use a version of dnssd.dll built from the free source code, I get an "unknown" exception returned from mDNSImports.DNSServiceBrowse. Do I need something besides this DLL to use your library?

Robbie Hanson said...

> If I uninstall Bonjour, but leave dnssd.dll on the
> machine or use a version of dnssd.dll built
> from the free source code

Is dnssd.dll the only thing that Bonjour installs? If you compile the library from apple's source code, isn't it still under the same terms?

Mugunth Kumar said...

I've published my service. Is there a callback that will let me know if a remote host has successfully resolved my service and is trying to connect?

I tried adding

publishService.DidResolveService += new NetService.ServiceResolved(publishService_DidResolveService);
publishService.DidNotResolveService += new NetService.ServiceNotResolved(publishService_DidNotResolveService);

But couldn't get control in the delegate methods.

Am I missing something?

Robbie Hanson said...

Hi Mugunth,

There is no such callback in the Bonjour API. This is because it doesn't make much sense to have one. The act of resolving a service only tells someone the IP address of the service - nothing else.

Some applications that use Bonjour automatically resolve discovered services right away. The application may never actually connect though. Other applications only resolve services on demand, immediately before connecting.

In other words, knowing when another Bonjour client resolves your IP address from your published service doesn't really tell you anything useful. You may, however, want to know when another client connects to your published service. And for this, you would build such functionality into the server component.

Servant said...

"Is dnssd.dll the only thing that Bonjour installs?"

Actually, no - one also needs mDNSResponder. It took me a while to figure out that a "Host" in Bonjour is any resident that participates in the mDNS protocol, not just service providers, and that on a Windows Host, the mDNS "service" is (usually) implemented as a Windows service. I built the mDNSResponder and it works great. For the record, then, for those wanting to implement just service discovery, you only need to build dnssd.dll and mDNSResponder.exe. (There's a bunch of other stuff in the source distribution that's either obsolete or related to other platforms.)

Thanks again for your help.

SeanD said...

Using the downloaded code, when I publish a service (using the publish app) I am seeing it 3 times in the browse app. Anyone else seeing this ? (and I don't have any other instances of it running).

Tope said...

Great sample app!. Can one send a file using this? When I try sending a pdf file.... I get an "invalid" error

Berndt Johansson said...

When running the application as a service the application uses a lot of cpu cycles but when running directly as a standalone application it works just fine. I have created a bug report on the code site with more information about the problem.

Anonymous said...

Tope, Bonjour is simply for discovering services. Once you've discovered a service, such as an FTP server, printer, etc, then you can do something with that service.

Anonymous said...

Hi All,

I tried BrowseserviceSample in ipV6 network but it does not work (I never get a call back ns_DidResolveService). I tried the original code from author and it works. Please let me know what I need to do to get this work on IPv6 environment.

Thanks,
Phuong

Anonymous said...

I think I found the problem. The IPLookup function only scan for IPv4 address, I need to add another scan for IPv6.

Andrija said...

Hello. I had same problem as Berndt Johansson. When running publish from service, my process where spending to much cpu time. Problem was because of the select loop. After select is unblocked client must call DNSServiceProcessResult. In sample app, this call was Invoked in async way thought Form. But in service you must set AllowMultithreadedCallbacks to true first( there is no form here :) ). If not, you will have endless select loop, and 50% of you CPU utilized :).

Anonymous said...

Someone said it above.
Bonjour will only tell you the IP:port of the service. Sometimes this is all people need. well this is all I need, does anyone know where I can get some sample code to do this?

David said...

Folks, first of all thanks for this library.

I'm unfortunately pretty bad at all that C interop stuff, and I'm getting an exception on the Marshal.FreeCoTaskMem(result) call in the GetVersion method. It only happens when running under x64 (the native call itself seems to work fine, it's just the dealloc that fails). Running this compiled specifically for x86 works. Anyone with more interop brains than me care to take a peek?

David said...

OK, so I figured out the x64 issue (I think). The external declaration should be:

[DllImport("dnssd.dll")]
public static extern DNSServiceErrorType DNSServiceGetProperty(
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(UTF8Marshaler))]String name,
IntPtr result,
ref UInt32 size);


and the relevant code in th e implementation around it should be:

DNSServiceErrorType error = mDNSImports.DNSServiceGetProperty(mDNSImports.DNSServiceProperty_DaemonVersion, result, ref size);

if (error != DNSServiceErrorType.kDNSServiceErr_NoError)
{
throw new DNSServiceException("DNSServiceGetProperty", error);
}

version = Marshal.ReadInt32(result);

Matt Dwen said...

The download link appears to be broken.

Robbie Hanson said...

Please use the google code page to download the software.

Melloware said...

David's solution above works on X64. Please add this code to the subversion trunk!

alexgcuesoft said...

I'm running on x64 Windows 7 and I've come across all kinds of issues. I did encounter David's problem, but I had to prototype it like..
[DllImport("dnssd.dll")]
public static extern DNSServiceErrorType DNSServiceGetProperty(
[MarshalAs(UnmanagedType.LPStr)]String name,
ref UInt32 result,
ref UInt32 size);
to get around that. Passing the result as a referenced int instead of intptr, that worked fine.

Both test apps just quit without exceptions when I ran them. I swapped out all the [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(UTF8Marshaler))] with [MarshalAs(UnmanagedType.LPStr)] and that kept it from quit/crashing. However, I'm not getting any callbacks called by the dll. The DNSRegisterService call succeeds but the callback is never called. Any ideas? The interop prototypes look good to me.

Kristian said...

Thanks for this code. Two comments:

It would be really cool if the changes that people have contributed in the comments here could be included in the SVN repo on google code. That would be reassuring for anyone wanting to rely on this project in production code.

Secondly, I checked out the current codebase from SVN and compiled it on Windows 7 64-bit. Bonjour is not installed. I do get the popups saying Bonjour is not installed, and I can see in the source that Application.Exit() is issued immedately afterwards. However, the code below this point is still run, resulting in an additional exception. Looking at the documentation for this Exit(), this actually makes sense. Adding a "return;" statement after Exit() fixes the problem.

Kristian said...

I have to correct myself, and alleborate a bit.

I -did- have Bonjour for Windows installed, but it was the 32 bit version even though I have Windows 64 bit. This combination results in the DLL for DNSSD not being found, as described in the last post. If I go into the project properties for the sample projects and change the platform target from "any cpu" to "x86", I get no error and they work.

However, this is an unfortunate limitation to put on projects that want to use this, so I go ahead and install the 64-bit Bonjour. I change back to "any cpu", and now I get a hard crash with no exception. Stepping through the VS2008 debugger shows it to be in Marshal.FreeCoTaskMem(result) (DNSService.cs:98), which I guess is the same crash some other commenters got. I get the same behaviour if I change from "any cpu" to "x64", not surprisingly. The strange thing is that if I change to "x86" it now works again. Not sure why.

I guess there have been made some assumptions about pointer size in the actual ZeroconfService library, which makes it only work on 32-bit?

Anonymous said...

As of 2/24/2010 Apple now supports Bonjour for C# and .NET natively! It also supposedly works on both 32 and 64-bit Windows.

Check it out at:

http://developer.apple.com/networking/bonjour/download/

margarito said...

herbal viagra
viagra alternatives

Anonymous said...

Has anyone implemented then new .NET DLL?
I haev tried to import this into VS2010 C#, but withour succes as it will not accept the DLL and a compatible COM/DLL.

I'm Sorry said...

is it possible to use iPhone to talk to .NET, after Bonjour discovery of services?
the sockets or streams on different platforms are compatible?

sorry...noob here...

Thanks in advance~

I'm Sorry said...

and anybody uses the Bonjour SDK from Apple?


Thanks in advance~

Aleks Totic said...

Just tried Apple's bonjour. It crashed in my Console application, but worked in forms. Deusty's solution just works on my MSVC10, Win7 32 bit setup.

prakash said...

Hello,

I want to develop chat application for windows and mac using bonjour so please tell how it possible. plese send me sample code or hint.. thanks a lot

Becker said...

Great resources here guys, this is very helpful. For my service I am setting up I need to broadcast a "subtype", but right now looks like netservice only supports "type" and not specifying a "subtype", does this sound correct?

carry on luggage size said...

Sensational info. Your post is such a refreshing someone to read through. thanks to share.