12.4 Elements Found in a Well-Written Library

When writing and designing a library, special care needs to be taken in the following areas:

12.4.1 Memory Management

A library needs two methods for allocating memory. When a thread enters the library, the library needs, at times, to allocate memory for the thread and to assign management of that memory to the thread. This is the default behavior of all memory functions such as malloc and calloc: the calling thread that requests and receives memory becomes the owner of the memory and is responsible for freeing the memory.

Sometimes a library needs memory do its work, rather than the thread's work. Because a library does over 99 percent of its work with threads from other NLM applications, the library needs a way of obtaining memory and retaining management responsibilities for that memory. This is the purpose of the library memory functions in libray.h. They allow the memory to be attributed to the NLM specified in the handle argument rather than to the owner of the thread that calls them.

The value for the handle argument can be obtained from one of the following:

  • The first argument to the _NonAppStart function or from the DLL_PROCESS_ATTACH message to the DllMain function (the lpvReserved parameter)

  • The getnlmhandle or uname2 function to be owned by the caller (and not merely one from a consuming NLM)

  • The findnlmhandle function

If possible, you should design your library to have your start up function (either _NonAppStart or DllMain) initialize all library resources by zeroing out data, reading in tables, and allocating and initializing blocks of memory. When this is not possible, you must call functions such as library_malloc and AllocSleepOK that use parameters to assign memory ownership rather calling functions such as malloc that use the identity of the calling thread to assign ownership. You need to remember to free such memory before unloading.

Besides direct memory allocation, you must take care that other allocated library resources are not inadvertently assigned to the client. In particular, you need to examine how synchronization primitives are initialized. There are no calls that allow you to assign ownership; the calling thread always becomes responsible. If the primitive, for example a mutex, is initialized to perform library work, the calling thread becomes responsible for this resource without being aware of its existence. To solve this problem, your library needs to create wrappered callbacks. Wrappering a callback using NX_WRAP_INTERFACE ensures proper resource attribution.

12.4.2 Interface Signatures and Prefixing

NetWare has a flat name space for symbols, which means that any symbols exported by your library can potentially collide with symbols export from any other library loaded on the server. To avoid collision, you should prefix all exported symbols with the name of your library in uppercase. This should be done for symbols that are exported in an import file and for symbols that are exported dynamically. For example, the following line exports two functions for LibC:

  EXPORT (LIBC) printf, vprintf
  

When you save this line to an import file and then link your application with it, the linker replaces all references in your code to the printf and vprintf functions with the prefixed names (LIBC@printf and LIBC@vprintf), which ensures that your application calls these functions from LibC rather than CLib. Prefixing is important to LibC and CLib because many of the functions in LibC are also found in CLib, and both libraries are always loaded on a NetWare server.

Even if you have designed your library to dynamically export symbols at runtime, these symbols should also be prefixed. These symbols face the same probability as symbols in import files of colliding with other exported symbols. For example:

  ExportPublicObject (nlmHandle, "FOOLIB@myFooFunction");
  

12.4.3 Protection of Client Instance Data

In your library design, resources obtained for one thread should not be consumable or made available to any other thread, especially threads owned by different applications. If an application has intimate knowledge of a library, a rare exception could be made to allow threads from this application to share library resources, but a better design would prevent this sharing. Under no conditions should threads be allowed to share data on the same stack.

12.4.4 Data Instancing

Data instancing is a problem on NetWare because, in both kernel and protected address space, more than one main can be running and thus more than one thread has access to any global variables. As you design your library, you need to be aware of the following types of instancing data:

Application Data

LibC exports a number of interfaces specially set up to help you handle problems of application data at the NLM or process level. The most important of these interfaces are get_app_data and set_app_data. These functions permit you to do the following:

  • Determine, in the context of any calling thread, whether that thread’s application has ever called your module before (get_app_data).

  • Get the block of allocated data for it if it has already been set up (get_app_data).

  • Set up a block of data, initialize it, and establish it (set_app_data) for the application.

Once established, the data is always there the next time any thread from that NLM calls your library. The destructor for this data is established when you call register_library or register_destructor from your library start up code.

The problem that arises on NetWare is that, because of registered callbacks, a calling thread doesn’t always have enough contextual identification to ensure that get_app_data can accurately identify the caller. It might decide that the caller has never called before. The solution to this is for your clients to wrapper all their callbacks. Correctly wrappering all callbacks using the interfaces in nks/netware.h solves the remaining problem of application data instancing.

While this is not something library code need concern itself with, it is useful to describe callback wrappering because while it occurs in application code, the lack of wrappering makes the library code appear to be inconsistent. Sometimes the library can find the application data, and sometimes it can't. For an example of wrappering a callback, see the following:

  #include <nks/netware.h>
  
  void  *gCBFRef = (void *) NULL;
  static int  CallBackFunc( int arg1, double arg2 );
  
  static int CallBackFunc( int arg1, double arg2 )
  {
     // do stuff here after getting called back by whatever service
     // you’ve registered with including a library
     // ...
     return 0;
  }
  
  void InitializeFunction( void )
  {   int   err; 
    // wrap the call-back...
    err = NX_WRAP_INTERFACE(CallBackFunc, 3, &gCBFRef);
     // ...}
  
  void CleanUpFunction( void )
  {
     // free up the call-back wrapper
     if (gCBFRef)
        NX_UNWRAP_INTERFACE(gCBFRef);
  }
  

The CallBackFunc function is made static here to emphasize the fact that it isn’t its address, but the wrapper’s, that will be communicated to the library for the purpose of calling. Do not be confused by this example and discussion. Your library client does not need to do this in order to call your interfaces. It is only when the address of a function inside the application is going to be communicated to that this needs to be done. For example in LibC if your application uses the RegisterForEventNotification function, you should wrapper your callback function so LibC knows who you are.

Wrappering must be done because the actual thread that executes the callback function is not one that belongs to the client application. In the course of the callback, the executing thread might use LibC interfaces that need context. For example, if the callback uses the open function, LibC needs the context to access the application’s open file descriptor table.

There is no way to find the application data without having put a context wrapper around the callback. The wrapper contains code to save the application identity and to set it as the thread’s owner for the duration of callback execution. After the callback is through, the original identity is restored by the wrapper.

Thread Data

Thread data takes a bit more work to handle because it cascades from application data. The thread-specific data cannot be identified without accurately knowing the application data. In combination with get_app_data, which gets the instance data for the application, your library can also discover whether per-thread data has been allocated, allocate it if it has not, or get a pointer to it if it has. This is done by creating a key with pthread_key_create at the time that the application instance data is allocated and set (using set_app_data) for the calling application.

Thereafter, upon getting application data, you can use the stored-away key index to call pthread_getspecific. At the time that the key is originally created, a destructor for the data associated with the key is registered if necessary.

12.4.5 Kernel and Protected Address Space Support

Libraries can be loaded either in the kernel or in protected address space. Ideally they are loaded in the same address space as the applications that use them. However, sometimes the ideal location is not possible:

  • If your library consumes kernel-only services, you cannot load it in a protected address space where it does not have access to those services.

  • If the library is loaded in the kernel, the consuming applications cannot themselves be loaded into a protected address space without special modification of your library.

LibC overcomes this problem by loading both in the kernel and in a protected address space. When a protected address space application uses a LibC service which is a kernel-only one (such as managing System V semaphores and other IPC mechanisms like FIFOs that must be shared across all address spaces), the LibC loaded in the kernel handles these issues indirectly and in cooperation with the instance that is loaded in protected address space. This involves writing marshalling code; for more information, see Section 12.6, Marshalling Interfaces.