Nat! bio photo

Nat!

Senior Mull

Twitter Github Twitch

The beauty of generic header-names

IMO one of the standout pieces of innovation in mulle-objc, is the pervasive use of and automatic generation of headers with generic names. I think it’s easiest to show by example, what I mean with “generic header-names” and how to achieve the ideal header flow with them.

The generic header-names concept is applicable to C and C++ as well, and is independent from mulle-objc.

Lets write an Objective-C library “my-symbol-lib”, that is supposed to lookup C symbols at runtime. It should provide a function NSStringFromSymbol that returns nil, if the symbol isn’t defined, and the symbol, if it is.

A first attempt looks like this:

NSStringFromSymbol.m:

#import <Foundation/Foundation.h>    // header for NSString
#include <dlfcn.h>                   // header for dlsym


NSString  *NSStringFromSymbol( NSString *s)
{
   if( ! s)
      return( nil);
   if( ! dlsym( RTLD_DEFAULT, [s UTF8String]))
      return( nil);
   return( s);
}

This isn’t really good enough yet for a library, because we also need a header file. So split it up into:

NSStringFromSymbol.h:

#import <Foundation/Foundation.h>    // header for NSString

NSString  *NSStringFromSymbol( NSString *s);

NSStringFromSymbol.m:

#import "NSStringFromSymbol.h"
#include <dlfcn.h>            // header for dlsym


NSString  *NSStringFromSymbol( NSString *s)
{
   if( ! s)
         return( nil);
    if( ! dlsym( RTLD_DEFAULT, [s UTF8String]))
       return( nil);
    return( s);
}

One important design decision at this point is, if <dlcfn.h> should be exposed in the header.

The downside of having <dlfcn.h> in the header are:

  • slower compilation times for your header consumers
  • symbol pollution for auto-complete
  • possibility of avoidable global symbol conflicts
  • header consumers may start to assume <dlfcn.h>’s presence although this is more or less an implementation detail

The upsides are:

  • it saves effort by header consumers that may need symbols from <dlfcn.h>

Sometimes it is unavoidable, if you need to expose types, symbols or constants that are defined by a foreign header and that are exposed by your own method and variable declarations. But this really ties your class tightly to the dependent library.

Beautification

Now here comes the beauty part. Instead of Foundation/Foundation.h and dlfcn.h lets use two generic header-names import.h and import-private.h. import-private.h is never to be used in any header but only in implementations.

NSStringFromSymbol.h:

#import "import.h"

NSString  *NSStringFromSymbol( NSString *s);

NSStringFromSymbol.m:

#import "NSStringFromSymbol.h"

#import "import-private.h"


NSString  *NSStringFromSymbol( NSString *s)
{
   if( ! s)
         return( nil);
    if( ! dlsym( RTLD_DEFAULT, [s UTF8String]))
       return( nil);
    return( s);
}

and

import.h:

#import <Foundation/Foundation.h>

import-private.h:

#import "import.h"
#include <dlfcn.h>

Lets also create a my-symbol-lib.h as the main include header to expose the API of this library.

my-symbol-lib.h:

#import "NSStringFromSymbol.h"

The required cross-platform headers Foundation/Foundation.h and NSStringFromSymbol.h are “streamed” to the API consumer, whereas the platform specific header dlfcn.h stays hidden:

Header flow

Advantages

Source motility

This is probably the major advantage for me. As the names of the headers are fixed, header and implementations files are now uniform in that regard and can be easily moved between libraries and executables. The “host” must provide the “glue” in import.h and import-private.h so that the sources have the required environment available.

One place for OS specifica

Assume our library has more than one implementation file, it is now easier to edit the one file import-private.h than many implementation files.

e.g.

#ifdef _WIN32
# include <whatever.h>
#else
# include <dlfcn.h>
#endif

Another effect of this is that the source files themselves are more readable and not littered with #ifdefs. I believe most of the arguments for single-file libraries become moot, when using generic header-names.

Code generation opportunity

As dependencies to headers outside of the library are contained in two files, it is much easier to use code-generation tools to manage the dependencies of a library. mulle-sde does just that.

Isn’t this the same as a pre-compiled header / <stdafx.h> ?

Its similar, but its not the same. First the clear separation between import.h and import-private.h is not reflected in a pre-compiled header. Second, you usually do not export a pre-compiled header to your API customers but you do so with import.h. Third pre-compiled headers that are unconditionally read on each compile are a non-portable hack.


But why are there so many more headers in mulle-objc ?

For libraries I generally like to be able to publish a C-interface to be used by C-only consumers. So there also exist include.h and include-private.h.

This will change in the next release, where you have to opt-in for C-header files for ObjC projects. That reduces the header count by 50% for most projects

For the auto-generation of header code it is generally easier, to not mix user edits and generated code in one file. Additional underscored headers are generated by the mulle-sde tools and included by the “human” editable files.

None of these files are really necessary. You can disable all or some of them with environment variables. See mulle-sde for more information on that.


Post a comment

All comments are held for moderation; basic HTML formatting accepted.

Name:
E-mail: (not published)
Website: