No navigation frame on the left?  Click here.

Net*(), NT, 95: A working sample

Have you read the comments on the Net(() and Win9X page yet? If not, please do so before tackling this sample.

This page is split into three sections here. The first one shows how to do it the hard way; the second one shows a shortcut that might save you some work; and the third one discusses radmin32.dll (and tries to do away with svrapi.dll).

Section 1: Blood And Gore

Before you grab the sample source and try to make sense of it, I'd like to add a few comments on the basic problems that have to be overcome in mixed NT/9X code using Net*() functions. Here are the six most important ones:

  1. Argument lists are different
  2. ANSI/Unicode
  3. Information levels are different
  4. Return structures are different
  5. Enumeration works differently
  6. Memory management is different

Items 1 and 4 means you cannot include the headers (LM.H and SVRAPI.H) in your source file; item 2 means different handling for input arguments and extracting info from the returned structures; item 3 means you have to determine in advance whether the target is an NT or a 9X machine.

The sample nt95.cpp shows the hard way to overcome these restrictions. Depending on the OS it is running on, it calls different functions to do the dirty work. The NT version of the function links dynamically to NETAPI32.DLL; note that I have, with name changes, replicated the necessary function and structure declarations near the top of the source file. The rest is pretty straightforward stuff.

The 95 version of the function jumps through some more hoops. It also links dynamically, but to SVRAPI.DLL, and the function prototypes and structures are differently named so as not to clash with the NT ones. After getting the function pointer, I have a little "for" loop that goes through the enumeration levels that interest us. First, I try level 10 which works against NT boxes and does not require administrative privilege; if that fails, I try level 50, which works against peer servers (i.e., other Win9X boxes).

To try a level, I initialize the buffer size to some small value and grab memory for the buffer. If the call returns saying it has more data to deliver, I release the buffer, grab a bigger one, and try again, until it works or I go over the 64K limit (the bufsize argument to the Win9X Net*() functions is a short int). This weird way to do things is a direct consequence of the lack of real enumeration capabilities. After that, it only gets worse; assuming I even get a meaningful result, I have to switch on the level actually used to get the result, so I can make sense out of the now filled-in buffer.

On the whole, this is as ugly as the night is dark, and the more functions you need, the worse it gets. So, without boring you any further, here it is:

nt95.cpp, 12 KB

Section 2: Band-Aids And Bandages

The main problem is not just the different code needed to handle all that stuff. No, the biggest pain is that you cannot even use the provided header files. Here is how to fix this:

Create two additional source files, one for all NT-specific code, the other one for all 9X code. (In the above sample source, that would only be one function per source file -- FillSessionListNT() and FillSessionList95()). In one file, include LM.H for the NT stuff, in the other one, include SVRAPI.H. Voilą, no more name conflicts ... but you still have to link dynamically to either NETAPI32.DLL or SVRAPI.DLL.

Next step: make sure that the functions you write take the same arguments (my sample above doesn't, yet) and return the same things (my sample does that). Compile each source file into its own DLL. Now you can even link one of your new DLLs to NETAPI32.DLL and the other one to SVRAPI.DLL; the only thing you have to do is dynamically load one of the two DLLs you just created, and off you go.

Final step: dynamically-linked DLLs are clumsy and unwieldy. VC++ 6.0 has a new feature, delay-loading, where you link to an import library (for us, both SVRAPI32.DLL and NETAPI32.DLL) -- but the DLL only gets loaded if any of its functions are called. You will still need two source files (to keep the headers separate), and you will still need to handle differently structured results, but the whole LoadLibrary()- and GetProcAddress() shebang goes out the window (and good riddance!).

So, get the stuff below and be sure to check out the linker options. You will find that:

I use the /export linker switch instead of a DEF file or __declspec(dllexport); this is one of my personal idiosyncrasies, the other methods work just fine;
nt95delayload.exe links implicitly to nt.dll and 95.dll, via their .lib files;
nt.dll links implicitly to netapi32.dll, via netapi32.lib;
95.dll links implicitly to svrapi.dll, via svrapi.lib;
the linker options for the two dlls contain one /delayload: switch each, and that they include delayimp.lib to make things work.

The ZIP file below should have its contents unzipped into some directory (but note the long filenames!). Run msdev.exe, open the nt95delayload.dsw file, and see the three projects appear. The build results appear in the …\dbg or …\rel subdirectory, depending.

Two more notes on building and running the sample: I use the newest Platform SDK with corrected headers. If you don't, you will need casts for the first three arguments to NetSessionEnum() in the NT code. If you also use the newest SDK, make sure that your include-directories (in Tools/Options…) have the SDK include directory before the VC++ one. Furthermore, you will have to update the VC++ runtime (MSVC*RT.DLL) before you can run the resulting executable on a Win9X machine (unless you already did that, of course).

nt95delayload.zip, 8 KB (contains long filenames)

Section 3: Radmin32 to the Rescue

The previous section demonstrated the use of /delayload: to painlessly load one of two DLLs, obviating the need for dynamic linking. This is all good and fine, but making access to SVRAPI.DLL (on Win9X) less painful may not help all that much, as SVRAPI.DLL contains a lobotomized mini-subset of the Net*() APIs, nothing else. Heck, you can't even build a SECURITY_DESCRIPTOR to protect an NT share with!

Microsoft faced the same problem when porting the NT administration tools (User Manager, Server Manager, Event Viewer, and so on) to Win95. Their solution to the problem is called RADMIN32.DLL, and if you have at least one NT Server, you also have that DLL -- it hides on the NT Server CD, in a subdirectory \CLIENTS, as part of the "NT Administration Tools for Windows 95" or so. I am not a lawyer, but the way I read the license, you are allowed to install the thing on every Win95 machine that has a per-seat client access license for any NT Server, or that accesses a concurrently-licensed NT Server through the temporary use of such a concurrent license. You are not allowed to redistribute the stuff (not even RADMIN32.DLL and RLOCAL32.DLL, which are all you need, and which would fit nicely into a redistribution package).

I will tell you up-front that RADMIN32.DLL has a huge drawback: Until a kind soul invests the time to create an import library, it is only dynamically linkable.

But it also has a huge advantage which, in my opinion, outweighs that drawback: It is all there. Everything. The Net*() APIs. The APIs that deal with SIDs, ACEs, and SECURITY_DESCRIPTORs. Event log functions. Service control, printers and print jobs, the LookupAccount*() calls, privilege lookup, the LSA API, a complete set of registry functions that will work against an NT registry with no hassles, right down to the I_Net*() and Sam*() functions if you are into undocumented things. Oh, and last but not least, a complete set of working Net*() APIs!

What do you do to use this treasure trove? As long as no one has produced an import library, you compile with the NT headers (one exception is discussed below), define some function pointers, and paste in the usual LoadLibrary() and GetProcAddress() shenanigans. At runtime you get your function pointers either from RADMIN32.DLL, or from whatever DLL the function lives in under NT (the "Quickinfo" in the functions' doc pages will tell you that). Everything else is the same from here on. Calling sequence, argument lists, returns, semantics, everything. Neat, huh?

The one exception mentioned above is the Net*() API group. Normally, every function that takes or returns a string exists in both Unicode and ANSI versions; thus, you have GetComputerNameA() and GetComputerNameW(), the latter being the wide-character (Unicode) version. A header #define then aliases GetComputerName to one of the two real functions. All good and well, so far. However, the Net*() APIs exist in Unicode only, and due to historical reasons, they do not follow the A/W convention. RADMIN32.DLL only has ANSI versions of those, and they have an "A" at the end, to avoid embarrassing accidents.

Therefore, to use the Net*() APIs, you will have to call GetProcAddress() for either "NetFooBarA" (Win95), or for "NetFooBar" (NT), and you will have to convert between ANSI and Unicode yourself. Also, a Win95 box lacks the RPC Server that actually executes the code for any Net*() API (and whose name is the first argument of a Net*() call), and that is why passing NULL or an empty string as the server argument will fail. (There is a smaller set of local functions which allow the administration of local shares etc.)

And here are the basic rules for using radmin32.dll and rlocal32.dll functions on Windows 9x:

First, get a list of functions in rlocal32.dll -- do that using "dumpbin /exports", because depends.exe doesn't really understand the export table in that DLL.

Locate the LocalNetGetDCName() function (you supply any missing "A" suffixes, please). You need that to find the DC that handles your logon requests, so you know which server to use for the other Net*() functions. From then on, you are free to call radmin32.dll functions against that server.

Rules for LocalFoo() functions (rlocal32.dll):

Start with the declaration for Foo() (without "Local") in the LM*.H headers.
All strings are ANSI; adjust argument types and structures accordingly. Yes, that means you have to write your own declarations for the datatypes and functions you need.
If the first arg to Foo() under NT is "server", then it is omitted for LocalFoo(). "Omitted" means, in this context, that it isn't there, not that you should pass NULL. In other words, LocalNetWkstaGetInfo() only has _two_ arguments.
Do the necessary LoadLibrary() shenanigans, and off you go.

Rules for Foo() functions (radmin32.dll):

Start with the declaration for Foo() in the LM*.H headers. Append an "A" to the names of functions that take or return strings.
All strings are ANSI; adjust argument types and structures accordingly. Yes, that means you have to write your own declarations for the datatypes and functions you need.
The "server" argument _must_ name an NT box, as \\SERVER. NULL or an empty string is not permitted, as these Net*() functions cannot query Win9x machines.
Do the usual LoadLibrary() shenanigans, and off you go.

A sample doing just that can be found here.

After a long delay, I finally sat down and did the import libraries for radmin32.dll and rlocal32.dll. And since an implib is such a nice thing to have, I decided to post the small thingummy that I used to produce the implib. To produce your very own radmin32.dll import library, download makedef.zip from the Miscellany section. Inside you will not only find the complete source but also a pre-built makedef.exe. Next, run "makedef AnyOldDLL.dll"; makedef will leave AnyOldDLL.def in the current directory. Then, run "lib /def:AnyOldDLL.def", and presto! you have AnyOldDLL.lib (and AnyOldDLL.exp, too). DEATH TO Q131313!