OSRLogo
OSRLogoOSRLogoOSRLogo x Subscribe to The NT Insider
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

Designing a Device API Part III: Exporting Functions

By Bruno van Dooren

In Part 1 of this series, I established why you should provide a user mode device API in a DLL package containing pure C style functions.

Part 2 followed with an explanation of the requirements for calling conventions and function declarations.

Now it is time for the metal to meet the meat, so to speak. It is time to explain how to actually export a function.

As said before: since we only export pure C style functions and we only use un-ambiguous declarations, this shouldn't be too hard either, right?

Indeed, this is true, but there are different ways of achieving this.

Exporting via Code Statements

The first way to export a function by name is to declare it in the code as "exported". You do this by preceding the function type with __declspec(dllexport) in the function declaration in the header file. This results in the function being accessible by name.

Of course, if the header is included in a client application, the function declaration should no longer be for export but for import, so again we will have to use a macro to provide the required functionality.

#ifdef BUILDING_MY_DLL

#define IMPEXP __declspec(dllexport)

#else

#define IMPEXP __declspec(dllimport)

#endif

IMPEXP void foo();

The idea is that the macro BUILDING_MY_DLL is only defined if you are building your DLL. That way the function foo is marked for export when you build the DLL, and marked for import if someone uses the header file for using the DLL.

The major problem with this way of exporting functions is that there is no way to prevent name decoration unless the functions all use the __cdecl calling convention. Any other calling convention causes your function names to turn ugly.

Exporting via a DEF File

The second way of exporting functions is to add a .DEF file to your DLL project. A def file is basically nothing but a text file that lists all functions that your DLL makes available. For example, the contents of a simple DEF file might look like the following:

LIBRARY "FunctionLib"

EXPORTS

foo

This example exports the function named "foo" from the DLL named FunctionLib.DLL. And because the example uses a DEF file, the linker will give it the name "foo" without regard for the name decoration rules that would otherwise make the function name very complex.

This behavior is generally what you want, because several client environments can only load extern functions if the programmer types in the complete decorated name. And as you can imagine, typing in dozens of names like 'foo@@YAHNHPA_W@Z' is not very nice.

Exporting via the Command Line

Finally, it is also possible to add /EXPORT switches to the linker command line to specify each function that is to be exported.

The disadvantage is that this forces you to work with fully decorated names like 'foo@@YAHNHPA_W@Z' because otherwise the linker won't find the functions. And if the function declaration changes, then so does the decorated name and you have to correct the linker command file.

This means your makefile or project file is filled with statements like this /EXPORT: foo@@YAHNHPA_W@Z

There are some scenarios in C++ in which this method of exporting would be a solution to a language specific problem, but never in our required environment of pure C.

So I mention this method for completeness, but for all practical purposes, forget it even exists.

Putting it All Together

There are quite a few lines of boilerplate code that have to exist simply to export one function. But the amount of boilerplate code stays the same, regardless of the number of exported functions, so don't let it scare you.

You Still Need the Import/Export Code

It's interesting to note that even though you typically use the DEF file for exporting functions, you still need the import and export pragmas.

The linker is smart enough to correct for mistakes in client code that doesn't have the import declarations in order. Stub functions will be generated in the client code by the linker to pretend that the function is local, while loading the external code under the hood.

This makes it seem as if it is not necessary to provide the import declaration., But the invisible layer that is now between the client application code and your DLL degrades runtime performance. True, the overhead is very low, but it's needless waste. And it might become a problem for someone, sometime.

And if the application programmer is using C or C++, then you don't want him to fudge in your headers to fix this for you. That might cause other problems that in the long run are going to cost you time, effort, money or all of the above.

So even if it seems pointless to provide the declaration specifications if you are using a DEF file, do it anyway.

Don't Stomp on Other People's Macros

By now we have two custom macros in our header file: DLL_CALLCONV and IMPEXP.

Both are relatively simple, and the chances are better than zero that someone else will eventually use macros with the same name. When that happens, they will clash with yours, and the client application programmer will have to manually edit header files to clean up the mess.

And you don't want him to do that. Instead, make sure that your macros are local to your header file only.

For the IMPEXP macro this is simple, because it is not needed outside of our header file. At the end of the header file, any previous definition of IMPEXP will be restored. This can be done with the push_macro and pop_macro

#pragma push_macro("IMPEXP")

#undef IMPEXP

#endif

// declarations go here

#pragma pop_macro("IMPEXP")

The DLL_CALLCONV macro is a bit trickier because it is needed outside of the header file. It also has to be used in the implementation of the function. But that is only true when you are using the header to build your DLL, and in that case the name clash is not a problem.

This means that when the header is included in a client project, the macro does not need to exist outside of the header. This can be achieved by some more macro magic.

#ifndef BUILDING_MY_DLL

#pragma push_macro("DLL_CALLCONV")

#undef DLL_CALLCONV

#endif

//define and use DLL_CALLCONV here

#ifndef BUILDING_MY_DLL

#pragma pop_macro("DLL_CALLCONV")

#endif

This way your DLL_CALLCONV macro will not leave the header file and pollute the global namespace unless you are building your DLL, in which case you can control the global namespace.

The Final Result

The code (see Figure 1) consists of the following parts (the declaration details are explained in the previous article):

  • Make sure that the functions have C linkage.
  • Macro magic to make sure the DLL_CALLCONV macro stays in the header.
  • Specify the calling convention, based on the platform for which the code is compiled.
  • Provide the macros that are needed to redirect calls to foo, based on whether the library is used in ANSI or Unicode builds.
  • Make sure the IMPEXP macro does not leave the header file.
  • Declare the functions for import or export, depending on how the header file is used.
  • Declare the version of foo that is used in ANSI applications.
  • Declare the version of foo that is used in Unicode applications.
  • Pop the IMPEXP macro from the macro stack so that we do not interfere with other headers
  • Pop the DLL_CALLCONV macro from the macro stack so that we do not interfere with other headers.
    •  

#ifdef __cplusplus //1

extern "C" {

#endif

#ifndef BUILDING_MY_DLL //2

#pragma push_macro("DLL_CALLCONV")

#undef DLL_CALLCONV

#endif

#ifndef _WIN64 //3

#define DLL_CALLCONV __stdcall

#else

#define DLL_CALLCONV

#endif

#ifdef UNICODE //4

#define foo fooW

#else

#define foo fooA

#endif

#pragma push_macro("IMPEXP") //5

#undef IMPEXP

#ifdef BUILDING_MY_DLL //6

#define IMPEXP __declspec(dllexport)

#else

#define IMPEXP __declspec(dllimport)

#endif

IMPEXP int DLL_CALLCONV fooA(wchar_t *Buffer, size_t BufferSize ); //7

IMPEXP int DLL_CALLCONV fooW(char *Buffer, size_t BufferSize ); //8

#ifdef __cplusplus

}

#endif

#pragma pop_macro("IMPEXP") //9

#ifndef BUILDING_MY_DLL //10

#pragma pop_macro("DLL_CALLCONV")

#endif

Figure 1

Of course you have to add a DEF file to your project and add "fooA" and "fooW" to the exports list.

The implementation for fooA would then by contained in one of the source files of your DLL project, and look like this:

int DLL_CALLCONV fooA(wchar_t *Buffer, size_t BufferSize )

{

//implementation omitted

return 0;

}

Conclusion

The three articles in this series now tell you what you need to know to design and implement a clean and maintainable interface for a C style DLL.

It looks scary and ugly, but remember: this is boiler plate code, and it stays the same, regardless of how many additional functions. But it has to be correct, or else you will have problems eventually. Sloppy work might cost you time, money and frustration.

And you can always copy-paste it into your next project, kind of like what you do when you create an INF file.

 

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