Introduction
I'm very new to C# as a programming language and to .NET as a framework. Coming
as I did from a C++ and MFC background I researched the alternatives and hinted
to my family that a copy of Tom Archers 'Inside C#' (2nd edition) would be a
much appreciated Christmas present. The hints were taken and it arrived in my
Christmas stocking. A good read and very well worth the money that I don't
offically know about :). Having read the book from cover to cover it was time to write some real C# code.
But I'm of the school of thought that says that one cannot truly learn a new
programming language by using examples targetted at just one or another feature
of the new language. I need a real project. Otherwise, if I attempt something and
it doesn't work the way I imagine it should it's all too easy to write it off. A real project presents
challenges that must be solved before the project can be said to be complete.
The Project
I chose a project that's pretty much going to be out of date in maybe a
years time, probably rather less. It's a SETI monitor that automatically discovers computers on my local network and
monitors the progress of instances of SETI running on each computer. This article doesn't present that project (though a future article might).
Instead, this article focusses on a class I wrote as part of the project to enumerate network resources. In Win32 you do this by using the
WNetOpenEnum()
API followed by (probably) multiple calls to the WNetEnumResource()
API.
A not so quick search of the .NET documentation didn't reveal any classes
implementing this functionality so it was time to write my own. Bear in mind
that this code was written by a veteran C++ programmer trying to move to C# :)
Moving a C++ function to C#
I have written code using the
WNetOpenEnum()
and
WNetEnumResource()
API's many times in the past, always in C++. So I was starting
from the standpoint of having written working code but without necessarily understanding all the nuances of the API. After all, if it works following the
MSDN examples tailored to my needs one moves on to other stuff right?
So I started with working C++ code. I won't present the code here. But let's discuss the API's. The first API is WNetOpenEnum()
. The
C/C++ prototype looks like this.
DWORD WNetOpenEnum(
DWORD dwScope,
DWORD dwType,
DWORD dwUsage,
LPNETRESOURCE lpNetResource,
LPHANDLE lphEnum
);
I won't go into all the details of the various parameters but I'll pass lightly over them. The
scope
parameter specifies whether you want to
enumerate global resources, remembered resources and suchlike. The
type
parameter lets you restrict enumerated resources to disk, print or all.
Usage
lets you specify such things as containers. (See the MSDN documentation for full details).
The lphEnum
is a pointer to a handle to an enumeration context which is used in subsequent calls to WNetEnumResource()
.
That leaves the lpNetResource
parameter.
All the parameters apart from the lpNetResource
parameter map directly onto C# datatypes. The lpNetResource
parameter doesn't. So
let's look at the C/C++ definition of that type.
The NETRESOURCE structure
typedef struct _NETRESOURCE {
DWORD dwScope;
DWORD dwType;
DWORD dwDisplayType;
DWORD dwUsage;
LPTSTR lpLocalName;
LPTSTR lpRemoteName;
LPTSTR lpComment;
LPTSTR lpProvider;
} NETRESOURCE;
All of the members map onto a C# datatype. DWORD
maps on the C# uint
type and LPTSTR
maps onto the string
type. So
far so good.
So let's see what a C# definition of a NETRESOURCE
structure might look like.
[StructLayout(LayoutKind.Sequential)]
private class NETRESOURCE
{
public ResourceScope dwScope = 0;
public ResourceType dwType = 0;
public ResourceDisplayType dwDisplayType = 0;
public ResourceUsage dwUsage = 0;
public string lpLocalName = null;
public string lpRemoteName = null;
public string lpComment = null;
public string lpProvider = null;
};
This is a literal copy of the
NETRESOURCE
structure from the C/C++ header file with the syntax massaged to make it digestible to the C# compiler. Well not
quite. Each
DWORD
member has been changed to an enum type, where the enum values are defined elsewhere within the class. The enum values are in turn
literal copies of the relevant C/C++ values massaged to make them digestible to the C# compiler. I did it this way for two reasons. The first is that I believe even the
original C++ structure should have been defined in this way. Changing the
DWORD
's into
enum
's buys some compile time parameter checking for free.
The second reason is that (at least in Visual Studio), Intellisense will kick in and help me remember the constants.
The WNetOpenEnum() function
The definition of the
WNetOpenEnum()
API, for C#, looks like this,
[DllImport("Mpr.dll", EntryPoint="WNetOpenEnumA",
CallingConvention=CallingConvention.Winapi)]
private static extern ErrorCodes WNetOpenEnum(ResourceScope dwScope,
ResourceType dwType,
ResourceUsage dwUsage,
NETRESOURCE p,
out IntPtr lphEnum);
That's pretty straightforward. Simply set up a call using the various
enum
values for the type of enumeration you want, pass it an instance of the
NETRESOURCE
structure and a reference to a place to store the returned enumeration handle. Calling code might look like this:
IntPtr handle = IntPtr.Zero;
ErrorCodes result;
result = WNetOpenEnum(ResourceScope.RESOURCE_GLOBALNET, ResourceType.RESOURCETYPE_ANY,
ResourceUsage.RESOURCEUSAGE_ALL, pRsrc, out handle);
The WNetEnumResource() function
Now we've got a handle to an open enumerator we use that handle to request information from the OS.
[DllImport("Mpr.dll", EntryPoint="WNetEnumResourceA",
CallingConvention=CallingConvention.Winapi)]
private static extern ErrorCodes WNetEnumResource(IntPtr hEnum,
ref uint lpcCount,
IntPtr buffer,
ref uint lpBufferSize);
This is where things get interesting. If you have another look at the
NETRESOURCE
structure you'll see that it contains 4 pointers to strings. An
obvious questions is where do those pointers point? In other words, where is the memory allocated? A few moments thought reveals that they can't be pointing at
strings inside the OS because in all likelihood those strings are in someone elses memory space. The API has to copy the strings into memory visible to the calling
process. Therefore, the memory into which the strings are copied must be allocated by the calling process. However the documentation says nothing about how large each
string buffer should be or even if we should point each pointer at a buffer allocated in our memory space. A close reading of the MSDN documentation for the
WNetEnumResource()
reveals this in the remarks.
An application cannot set the lpBuffer parameter to NULL and retrieve the required buffer size from the lpBufferSize parameter. Instead, the application
should allocate a buffer of a reasonable size (16 kilobytes is typical) and use the value of lpBufferSize for error detection.
Aha! Obviously the WNetEnumResource
API reserves the first few bytes of the memory buffer for an instance of the NETRESOURCE
structure and
copies the strings into the same buffer after that structure. So we're expected to allocate a buffer large enough to hold the NETRESOURCE
structure
and all 4 strings. In C++ this is trivial. And doubtless, as I become more proficient with C# and the .NET Framework, it'll become trivial there too :)
Incidentally, this also reveals how old these API's are. I'm sure the structure would have used BSTR
's to solve the memory allocation problem had they
existed at the time this structure was defined.
A hidden gotcha
Another close reading of the documentation for
WNetEnumResource()
says that if you set the
lpCount
to X the function will return
X results if they will fit into the buffer. If not it will return as many results as will fit in the buffer and set the value pointed at by
lpCount
to
the number of returned results. But in practice it doesn't seem to work that way. All my tests in C++ return just 1 result per call regardless of how large the
buffer is and what I specify as the desired count. *shrug*
The WNetCloseEnum() function
As you'd expect, having opened a handle and used it, you're required to close it when you've done with it. That's done with the
WNetCloseEnum()
API.
[DllImport("Mpr.dll", EntryPoint="WNetCloseEnum",
CallingConvention=CallingConvention.Winapi)]
private static extern ErrorCodes WNetCloseEnum(IntPtr hEnum);
Putting it all together
So we've had a look at the 3 API's and the structure we need to enumerate servers on our local network. Here's the enumeration function itself:
private void EnumerateServers(
NETRESOURCE pRsrc,
ResourceScope scope,
ResourceType type,
ResourceUsage usage,
ResourceDisplayType displayType)
{
uint bufferSize = 16384;
IntPtr buffer = Marshal.AllocHGlobal((int) bufferSize);
IntPtr handle = IntPtr.Zero;
ErrorCodes result;
uint cEntries = 1;
result = WNetOpenEnum(scope, type, usage, pRsrc, out handle);
if (result == ErrorCodes.NO_ERROR)
{
do
{
result = WNetEnumResource(handle, ref cEntries, buffer, ref bufferSize);
if (result == ErrorCodes.NO_ERROR)
{
Marshal.PtrToStructure(buffer, pRsrc);
if (pRsrc.dwDisplayType == displayType)
aData.Add(pRsrc.lpRemoteName);
if ((pRsrc.dwUsage & ResourceUsage.RESOURCEUSAGE_CONTAINER) ==
ResourceUsage.RESOURCEUSAGE_CONTAINER)
EnumerateServers(pRsrc, scope, type, usage, displayType);
}
else if (result != ErrorCodes.ERROR_NO_MORE_ITEMS)
break;
} while (result != ErrorCodes.ERROR_NO_MORE_ITEMS);
WNetCloseEnum(handle);
}
Marshal.FreeHGlobal((IntPtr) buffer);
}
This is almost trivial. We pass a
NETRESOURCE
structure and a
bunch of constants to the function. We then open the enumeration and if that
succeeds we go into a loop calling
WNetEnumResource()
for each network resource. For each resource whose type matches the type passed as a parameter we add the remote name member of the structure to an array. For each resource which has the
ResourceUsage.RESOURCEUSAGE_CONTAINER
bit set we recursively call the function.
But wait a minute! What's with the Marshal.AllocHGlobal()
call? That's to allocate a block of memory to serve as the buffer which the
WNetEnumResource()
API will treat as a NETRESOURCE
structure followed by buffer space for the strings.
When we get the buffer back after the call we use Marshal.PtrToStructure()
to copy the NETRESOURCE
portion of the buffer into the structure
instance passed to the function. When the copy is done the string
members of the structure still point at the strings allocated within the buffer. I
struggled mightily with this (doubtless because of my C++ background). It took seeming ages to understand that even with the unsafe
keyword bracketing
code the compiler wasn't about to let me simply cast the buffer into a NETRESOURCE
structure without some .NET Framework intervention. My thanks go to
Heath Stewart for pointing me in the right direction.
Other things about the class
Because the class is used to enumerate a list of servers it makes sense for it to implement the
IEnumerable
interface to allow it to be used inside a
foreach
statement. I used a trick Tom Archer mentions in his book. Because the class uses an
ArrayList
to store the list of enumerated servers
and that class already implements
IEnumerable
my
GetEnumerator()
implementation simply returns the enumerator for the
ArrayList
.
public IEnumerator GetEnumerator()
{
return aData.GetEnumerator();
}
What could be simpler than that?
The enumerator returns strings, so one might use it like this.
ServerEnum servers = new ServerEnum(ResourceScope.RESOURCE_GLOBALNET,
ResourceType.RESOURCETYPE_DISK,
ResourceUsage.RESOURCEUSAGE_ALL,
ResourceDisplayType.RESOURCEDISPLAYTYPE_SHARE);
foreach (string s1 in servers)
Console.WriteLine(s1);
Which example would enumerate all shares on the network. If the second parameter were changed to
ResourceType.RESOURCETYPE_PRINT
a list of all printer
shares would be returned. The sample project download is compiled to show a list of servers on the network. So the relevant code looks like this.
ServerEnum servers = new ServerEnum(ResourceScope.RESOURCE_GLOBALNET,
ResourceType.RESOURCETYPE_DISK,
ResourceUsage.RESOURCEUSAGE_ALL,
ResourceDisplayType.RESOURCEDISPLAYTYPE_SERVER);
foreach (string s1 in servers)
Console.WriteLine(s1);
Control of enumerated results
The bunch of constants that are passed to the
EnumerateServers
function are used for fine control of the output. The following tables are from
the MSDN Library April 2000.
The ResourceScope
constants and their meanings are:
Value |
Meaning |
RESOURCE_CONNECTED |
Enumerate currently connected resources. The dwUsage member cannot be specified. |
RESOURCE_GLOBALNET |
Enumerate all resources on the network. The dwUsage member is specified. |
RESOURCE_REMEMBERED |
Enumerate remembered (persistent) connections. The dwUsage member cannot be specified. |
The ResourceType
constants are:
Value |
Meaning |
RESOURCETYPE_ANY |
All resources. |
RESOURCETYPE_DISK |
Disk resources. |
RESOURCETYPE_PRINT |
Print resources. |
The ResourceUsage
constants are:
Value |
Meaning |
RESOURCEUSAGE_CONNECTABLE |
The resource is a connectable resource; the name pointed to by the lpRemoteName member can be passed to the
WNetAddConnection() function to make a network connection. |
RESOURCEUSAGE_CONTAINER |
The resource is a container resource; the name pointed to by the lpRemoteName member can be passed to the
WNetOpenEnum() function to enumerate the resources in the container. |
And, finally, the ResourceDisplayType
constants are:
Value |
Meaning |
RESOURCEDISPLAYTYPE_DOMAIN |
The object should be displayed as a domain. |
RESOURCEDISPLAYTYPE_SERVER |
The object should be displayed as a server. |
RESOURCEDISPLAYTYPE_SHARE |
The object should be displayed as a share. |
RESOURCEDISPLAYTYPE_GENERIC |
The method used to display the object does not matter. |
The last set of constants, ResourceDisplayType
are confusing. The documentation seems to say that it's a hint as to how to display the data. But it
actually seems to behave as a filter.
You apply various combinations of these constants to filter the results returned by the network enumerator. For example, if all you're interested in is a list
of servers on the network you'd use ResourceScope.RESOURCE_GLOBALNET
, ResourceType.RESOURCETYPE_ANY
, ResourceUsage.RESOURCEUSAGE_CONTAINER
and ResourceDisplayType.RESOURCEDISPLAYTYPE_SERVER
.
Note that the class presented in the download includes some constants that aren't documented in MSDN but that are present in the C++ header files. In particular
there are many more constants defined for the ResourceDisplayType than MSDN documents. On my system most of them result in nothing being returned at all, but
then this is my system, I have a workgroup, not a domain or an Active Directory. I included them for the sake of completeness.
Acknowledgements
I'd like to thank Marc Clifton for reviewing this article for me. As it was my first C# article I was somewhat nervous about posting it so I chose to ask an MVP
for review and advice. Marc was very helpful and patient.
History
28 February 2004 - Initial version.