OSRLogo
OSRLogoOSRLogoOSRLogo x OSR Custom Development Services
OSRLogo
x

Everything Windows Driver Development

x
x
x
GoToHomePage xLoginx
 
 

    Thu, 14 Mar 2019     118020 members

   Login
   Join


 
 
Contents
  Online Dump Analyzer
OSR Dev Blog
The NT Insider
The Basics
File Systems
Downloads
ListServer / Forum
  Express Links
  · The NT Insider Digital Edition - May-June 2016 Now Available!
  · Windows 8.1 Update: VS Express Now Supported
  · HCK Client install on Windows N versions
  · There's a WDFSTRING?
  · When CAN You Call WdfIoQueueP...ously

Guest Article: Designing a Device API - Part II: Function Declarations

By Bruno van Dooren

In a previous article (Designing a Device API Part I, The NT Insider, Jan-Feb 2007), I established why you should provide a user mode device API in a DLL package containing pure C style functions. That leads us automatically to the next topic: function declarations.

Since we only export pure C style functions, that shouldn't be too hard, right?

In a way this is true, but there is more to it than meets the eye at first:

  • The code should compile cleanly for 64 bit platforms as well as 32 bit platforms.
  • If you use strings in function parameter lists, judge whether there is a point is supporting both ANSI and UNICODE strings. Do not support only ANSI.
  • The complete export definition should be unambiguous. I.e. everything that has an effect should be specified, not implied.

It sounds simple but it is easy to make mistakes.

32 vs 64 bit
Gone are the days where we all had comfortable lives where pointers were always 32 bits, and long, int and DWORD all meant the same thing on all consumer platforms.

32 bit processing will be around for years to come, but 64 bit is also rapidly coming up. Lots of server applications have been running on 64 bit platforms for some time now, if only because they really need more than a paltry 3GB of memory to do their work.

You as the developer are stuck in the middle. Your code preferably runs on both platforms because otherwise you are missing out on a significant part of the market. 32 bit DLLs can be used on a 64 bit platform, but only in 32 bit applications. Furthermore, 32 bit emulation brings with it a number of issues with COM, the registry, the filesystem and a lot of other annoying details.

If you develop DLLs that will be deployed on other systems, you really want to be able to compile your code for both platforms. In order to facilitate this, there are a number of rules that you have to follow. This chapter only touches on the subject of keeping your DLL interfaces 64 bit clean, not your entire code base.

Size Does Matter
Despite what some men say, size is really important. The most important considerations can be summed up concisely:

  • Use the size_t  type for any parameter that confers size information about structures, buffers or other things that live in blocks of memory.
  • Use types like __int8, __int16, __int32 and __int64 instead of char, short, int and long long.

Be aware that structure sizes and default alignment are different between 32 and 64 bit compilation. If you work with structures and buffers, investigate whether these differences have an impact on your DLL, and what you can do about it.  As long as you design the interface of your functions with these issues in mind, there should be no unforeseen problems.

ANSI vs Unicode
If you browse through the Windows header files, you see that a lot of Windows platform functions are defined as macros that get expanded at compile time.

For example the function 'LoadLibrary' is declared in winbase.h as:

ifdef UNICODE
#define LoadLibrary  LoadLibraryW
#else
#define LoadLibrary  LoadLibraryA
#endif

The reason for this can be found in the actual function declarations (without the surrounding distractions) for LoadLibraryA and LoadLibraryW:

LoadLibraryA(
    LPCSTR lpLibFileName);
LoadLibraryW(
    LPCWSTR lpLibFileName);

If your program is compiled for either ANSI or multi-byte character sets, 'LoadLibrary' gets expanded to 'LoadLibraryA', which expects a pointer to a string buffer of 8 bit characters. If your program is compiled for Unicode instead, 'LoadLibrary' gets expanded to 'LoadLibraryW', which expects a pointer to a string buffer of 16 bit Unicode characters.

The Windows platform itself uses Unicode strings, as does the .NET framework.

If you work with raw text buffers, you should at least support Unicode. Use ANSI as well if you have a case for it, but don't support ANSI only because every string buffer has to be marshaled back and forth between Unicode and ANSI - this is error prone, and will have an unnecessary performance impact

Don't You Not Hate Unambiguity Or Not?
Ambiguity in an interface contract can cause a lot of problems. If your DLL exports a given function, you want the function declaration to fully describe the interface of that function.

The true meaning of a function declaration depends on several things. Each of them has a default setting that applies during compilation, but they should all be specified explicitly so that whoever looks at your function declarations knows exactly how it is supposed to behave.

Calling Conventions
A calling convention is a contract that specifies the exact procedure for calling a function. It determines in which order the function arguments are pushed on the stack, what happens with the registers, and who pops parameters from the stack.

There are lots of calling conventions, with all sorts of behavior. The most commonly used are __cdecl (the C calling convention), __stdcall (the standard win32 calling convention), and __fastcall (a variation of __stdcall).

Apart from the technical differences that few people know or care about, each of those has one feature that makes it important.

  • __cdecl dictates that the caller removes parameters from the stack. This makes it possible to create functions with variable arguments, since only the caller knows how many parameters were put on the stack in the first place.
  • __stdcall dictates that the callee removes the parameters from the stack. This prohibits variable length parameter lists since the callee only knows how many parameters to pop if the parameter list was fixed curing compilation. __stdcall is also the calling convention that is used by the Windows platform itself. Any function that is exported from a DLL and uses this convention can be used by any programming language that runs on the Windows platform simply because that is the convention that is used for the system DLLs.
  • __fastcall uses both the stack and registers to pass parameters. It is the one and only convention that is used by Windows and the VC++ compiler for 64 bit platforms.

For 32 bit environments, the best option would be to use __stdcall because anything that runs on the Windows platform can use it.

If you really need variable length arguments, you have to use __cdecl, but there are few valid reasons for such functions. They are dangerous to use because any mistake in using them has the potential to crash an application or destabilize the system.

For a 64 bit environment you will use __fastcall, but you don't specify it because the compiler does not allow you to. Any calling convention you specify will be ignored. Therefore it is best not to specify anything to avoid confusion.

If you do not specify a calling convention, you'll get the default of your C++ environment. In a default installation of VC++ this is __cdecl. You don't want to rely on it though, because different environments might have different default configurations, and __cdecl is not what you want to use anyway.

Since the same declaration will be compiled for both 32 and 64 bit environment, a macro has to be used to facilitate this.

  1. For 64 bit builds, the calling convention is empty. For 32 bit builds the calling convention is defined to be __stdcall. That way we get clean builds in all configurations.
  2. Declare functions that will eventually be exported.

#ifdef  _WIN64                        //1
#define FOO_DLL_CALLCONV 
#else
#define FOO_DLL_CALLCONV __stdcall
#endif

void CALLCONV foo();                  //2

This way, function 'foo' will have the correct calling convention while at the same time removing the calling convention specification for 64 bit builds.

C vs C++: What's In A Name
A common problem with DLL is this: You're compiling a program, and at the linking stage you link against the import lib file of a certain DLL that exports a function 'foo'.

You get a linker error that indicates that the linker cannot find function 'foo', despite the fact that you know for certain it exists and is exported.

What's happened is that when the exported function was compiled, its name was decorated or 'mangled' by the C++ compiler.

C++ allows function overloading, where different functions can have the same name but a different parameter list.

Such functions cannot have the exact same name or there would be a naming conflict. That is why the types in the parameter list are encoded in the name of the function. For example, the function 'foo' might have the name 'foo@@YAHNHPA_W@Z' in the resulting object file.

In C this is not the case. C assumes that a function named 'foo' will still be the only one called 'foo'. C never needed name decoration to support its language features.

Since this feature of C++ makes it very hard to properly use the DLL in a C environment, or any other environment that requires us  to specify a function by its true name, we have to prevent this from happening.

We solve this with the following construction:

#ifdef __cplusplus
extern "C"{
#endif

void CALLCONV foo();

#ifdef __cplusplus
}
#endif

The __cplusplus macro is always defined when a C++ compiler is invoked. In this case the function 'foo' is declared as a function that has to be compatible with C linkage rules using the extern 'C" statement.

Putting It All Together
Finally, all the elements are in place to make a proper function declaration. For this example we declare a function that prints a text string in a user supplied buffer.

  1. Prevent symbol names from getting mangled C++ style.
  2. Define the calling convention for our DLL.
  3. Our hypothetical function takes a string buffer as a parameter. If we want to support both ANSI and Unicode, we have to implement 2 distinct functions. This also means that the pseudo name 'foo' can only be use in C or C++ environments. Environments that need the programmer to specify the function foo by name need to use fooA or fooW.
  4. Declare fooA.

declare fooW
#ifdef __cplusplus                    //1
extern "C" {
#endif

#ifndef _WIN64                        //2
#define DLL_
CALLCONV  __stdcall
#else
#define DLL_CALLCONV
#endif

#ifdef UNICODE                        //3
#define foo  fooW
#else
#define foo  fooA
#endif
int DLL_CALLCONV fooA(wchar_t *Buffer, size_t BufferSize );     //4
int DLL_CALLCONV fooW(char *Buffer, size_t BufferSize );     //5

#ifdef __cplusplus
}
#endif

As you can see, once you know what the different pieces of the puzzle look like, assembling it is not a big deal.

Conclusion
This article lays down the ground rules of what a function declaration should look like if that function has to be exported from the DLL later on.  My next article will discuss the 'how' of function exports.

Bruno van Dooren is a systems engineer for Genzyme Flanders and a Microsoft MVP, specializing in Visual C++.  He can be reached at bruno_van_dooren@hotmail.com. He also maintains a blog at www.msmvps.com/blogs/vanDooren where he blogs about topics related to C++ and the Windows platform.

User Comments
Rate this article and give us feedback. Do you find anything missing? Share your opinion with the community!
Post Your Comment

Post Your Comments.
Print this article.
Email this article.
bottom nav links