bottom prev next

Handle

The Handle pattern may be used to encapsulate a data type by separating its interface from implementation and by making the former public and the latter private. A variable of a data type encapsulated as Handle can be manipulated via its interface only. Handle is packaged into a header file and a shared library.

The Handle pattern consequences:

  • Data type's interface is separated from its implementation at compile and run times
  • Changes to the data type's implementation do not affect its users
  • Only one implementation can be attached to a given interface
  • Short of using alloca() the traditional stack frame variables of this data type can not be created


Sample Problem

Implement a custom string data type.


Generic Solution Description

1) Declare an incomplete data type in a public header file. Either forward declaration, struct type, or a custom type definition, typedef void type_t, can be used.

2) Make all the public functions accept a pointer to an incomplete data type.

3) Implement all the public and private functions and the incomplete data type in its entirety in a .c file. If multiple implementation .c files are needed then put a complete data type definition in a private header file and share that file only with this data type's (private) implementation files - Handle's users should not include it in their code.

4) Package Handle's implementation files into a shared library.


Sample Solution Implementation

Step 1

In a public header file libstr.h define an incomplete data type as typedef void str_t (you are encouraged to also experiment with a forward declaration).

libstr.h:

#include <sys/types.h> typedef void str_t;


Step 2

Add the prototypes of the public life cycle functions to the public header file. Because void is a special data type in C make all the public functions accept a "pointer to" str_t.

The following functions will be present in the form shown below in any and all the data types implemented as Handle. What will most likely differ from one data type to another is the seed information supplied to typeNew() and typeConstruct(). The concrete nature of the custom data type at hand will dictate what that information should be.

In our particular case it makes sense to pass an optional pointer to a null-terminated C string to strNew() and strConstruct().

libstr.h:

#include <sys/types.h> typedef void str_t; /* Public interface. Life cycle functions. */ extern str_t* strNew( const char* ); extern void strDelete( str_t** ); extern str_t* strConstruct( void*, const char* ); extern void strDestruct( str_t* ); extern size_t strSizeOf();


Step 3

In the public header file add the prototypes of the public functions that will make the custom data type functional and usable. We will start with a comparison function strCmp().

libstr.h:

#include <sys/types.h> typedef void str_t; /* Public interface. Life cycle functions. */ extern str_t* strNew( const char* ); extern void strDelete( str_t** ); extern str_t* strConstruct( void*, const char* ); extern void strDestruct( str_t* ); extern size_t strSizeOf(); /* Compare two strings: */ extern int strCmp( str_t*, str_t*, int );


Commentary

When Handle is built into a shared library its header file is copied into and expanded in place in the corresponding .c file(s) by the C preprocessor as part of the execution of the #include directive. The C compiler proper operates only in terms of translation units - it knows nothing about the header files. As such, while processing the corresponding .c file, it will first encounter the Handle's incomplete data type. However, later on, the C compiler should discover:

- the relevant functionality implementation in terms of a complete data type;

- a way to switch between the incomplete and complete data types;

For that to occur we will craft our string in a private, libstr.c, file in terms of a private complete data type strimpl_t visible only inside this private implementation file. To switch between str_t and strimpl_t we will use the C typecasting mechanism. It follows then that str_t will play the role of a handle - an opaque pointer - to the underlying data type's state variables.

The current translation unit, libstr.c, will have references to two data types. The only references to the incomplete data type will be of type "pointer to" or str_t*. The complete data type will be referenced directly, as strimpl_t, and as a "pointer to" or as strimpl_t*. As such, the C compiler will have no problems converting the human readable source code file, libstr.c, into an object file, libstr.o, and the link editor will have no problems packaging that object file into a fully functional shared library, libstr.so.

To exercise the functionality of our string users will first include its public header file, libstr.h, in their code which will go through the same motions: eventually the C preprocessor will expand libstr.h and the C compiler will encounter our Handle's incomplete data type definition, typedef void str_t. However, because the only references to str_t will be of type "pointer to" or str_t*, the C compiler will have no problems converting the current translation unit into the corresponding object file.

However, if the link editor is asked to construct a program - not a library - then, in the absence of the incomplete data type's full definition and implementation, it will first complain about the unresolved references to the encountered string public functions and then it will fail.

This is where we will close the circle by instructing the link editor to use the Handle's shared library via, perhaps, the -l directive. Whether the path to the Handle's shared library is recorded in the executable image at link time or discovered dynamically at load time, the object files comprising that library will supply the required definitions and the link editor will be able to assemble a fully operational program.


Step 4

Implement the string functionality in a private file, libstr.c, which will delineate the visibility scope of the complete data type strimpl_t.

If your string library has to be split across multiple .c files then define strimpl_t in a private header file, libstrimpl.h perhaps. Include this private header file in the string's private source files only and nowhere else.

For demonstration purposes we have added an informative printf() in strCmp().

libstr.c:

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <strings.h> #include "libstr.h" /* Complete (implementation) string data type. Only visible inside this (private) file. */ typedef struct { size_t bufsz; char* buf; } strimpl_t; extern size_t strSizeOf() { return sizeof( strimpl_t ); } extern str_t* strNew( const char* istr ) { void* simpl = calloc( 1, strSizeOf() ); str_t* s = strConstruct( simpl, istr ); return s; } extern str_t* strConstruct( void* smem, const char* istr ) { strimpl_t* s = ( strimpl_t* )smem; if ( istr && *istr ) { s->bufsz = strlen( istr ); } else { s->bufsz = 32; } s->bufsz += sizeof( char ); s->buf = calloc( s->bufsz, sizeof( char ) ); if ( istr && *istr ) { memcpy( s->buf, istr, s->bufsz ); } else { s->buf[ 0 ] = '\0'; } return ( str_t* )s; } extern void strDestruct( str_t* s ) { strimpl_t* str; if ( !s ) { return; } str = ( strimpl_t* )s; if ( str->buf ) { /* Balances calloc() in strConstruct(). */ free( str->buf ); } memset( str, 0, sizeof( strimpl_t ) ); } extern void strDelete( str_t** s ) { if ( !s && !*s ) { return; } strDestruct( *s ); /* Balances calloc() in strNew(). */ free( *s ); *s = ( str_t* )NULL; } extern int strCmp( str_t* s1, str_t* s2, int caseSensitive ) { int rv; int ( *scmp )( const char*, const char*, size_t ); strimpl_t* str1 = ( strimpl_t* )s1; strimpl_t* str2 = ( strimpl_t* )s2; printf( "%s:%d: strCmp( \"%s\", \"%s\", %d )\n", __FILE__, __LINE__, str1->buf, str2->buf, caseSensitive ); scmp = caseSensitive ? strncmp : strncasecmp; rv = scmp( str1->buf, str2->buf, strlen( str1->buf ) ); return rv; }


Step 5

Package the string files into a shared library:

gcc -g -c -fPIC -I . libstr.c gcc -g -shared -o libstr.so libstr.o


Step 6

Write a sample program to exercise the Handle pattern.

str.c:

#include <stdio.h> #include "libstr.h" extern int main( int argc, char* argv[] ) { str_t* s1; str_t* s2; if ( argc != 3 ) { return 1; } s1 = strNew( argv[ 1 ] ); s2 = strNew( argv[ 2 ] ); printf( "Strings s1 and s2 are %s equal.\n", strCmp( s1, s2, 1 ) ? "not" : "" ); strDelete( &s1 ); strDelete( &s2 ); return 0; }

In the code above variables s1 and s2 play the role of run time handles while the data type str_t plays the role of a compile time handle. In either case a handle is an opaque pointer to the entity's state which exists in distinct separation from its interface.


Step 7

Build the sample application. We will link the string library in explicitly:

gcc -g -c -I . str.c gcc -g -L . -o str str.o -lstr


Step 8

Run the sample application with various inputs:

./str hello world libstr.c:117: strCmp( "hello", "world", 1 ) Strings s1 and s2 are not equal. ./str hello hello libstr.c:117: strCmp( "hello", "hello", 1 ) Strings s1 and s2 are equal.


Cost Analysis

The sum total of all the files that must be modified to accommodate a change to strimpl_t is equal to three:

1) libstr.c
2) libstr.o
3) libstr.so

Proof:

Let us assume that to improve the performance of our string library we want to add an extra state variable that will store the actual length of the current string at all times, slen. This state variable is not to be confused with the already existing state variable that stores the size of the allocated buffer, bufsz.

The steps required to implement the above change are:

- Add size_t slen to strimpl_t in libstr.c:

typedef struct { size_t bufsz; char* buf; size_t slen; } strimpl_t;

- Addjust strConstruct() and strCmp() in libstr.c:

extern str_t* strConstruct( void* smem, const char* istr ) { ... s->buf = calloc( s->bufsz, sizeof( char ) ); if ( istr && *istr ) { memcpy( s->buf, istr, s->bufsz ); s->slen = s->bufsz - sizeof( char ); } else { s->buf[ 0 ] = '\0'; s->slen = 0; } return ( str_t* )s; } extern int strCmp( str_t* s1, str_t* s2, int caseSensitive ) { int rv; int ( *scmp )( const char*, const char*, size_t ); strimpl_t* str1 = ( strimpl_t* )s1; strimpl_t* str2 = ( strimpl_t* )s2; printf( "%s:%d: strCmp( \"%s\", \"%s\", %d )\n", __FILE__, __LINE__, str1->buf, str2->buf, caseSensitive ); scmp = caseSensitive ? strncmp : strncasecmp; rv = scmp( str1->buf, str2->buf, str1->slen ); return rv; }

- Rebuild the libstr.o file:

gcc -g -c -fPIC -I . libstr.c

- Rebuild the libstr.so file:

gcc -g -shared -o libstr.so libstr.o

The str application is impervious to the above change.


From Here

Using string length as a sample new public function here is how to add it to our string's interface:

- Add a new function prototype to the public header file, libstr.h:

extern size_t strLength( str_t* );

- Implement the new public function in the libstr.c file:

extern size_t strLength( str_t* s ) { strimpl_t* str = ( strimpl_t* )s; return str->slen; }

- Rebuild the string library:

gcc -g -c -fPIC -I . libstr.c gcc -g -shared -o libstr.so libstr.o

- Rebuild the relevant dependencies on the string library only if they have been modified to reflect the changes in str_t's public interface. Observe here that the dependencies that wish to ignore these changes do not have to be rebuilt. For example, if we have added strLength() to our string library then without any further changes, as is, our sample program str will work without recompilation.


Automation

To eliminate the repetitive typing it is possible to automate the initial Handle's implementation process. Below is a sample template consisting of two files, libhandle.h and libhandle.c, which contain the name TYPE as a stand in for the name of the data type of your choice, the initial set of life cycle functions and one sample data type manipulation function.

libhandle.h:

#include <sys/types.h> typedef void TYPE_t; extern TYPE_t* TYPENew(); extern void TYPEDelete( TYPE_t** ); extern TYPE_t* TYPEConstruct( void* ); extern void TYPEDestruct( TYPE_t* ); extern size_t TYPESizeOf(); extern int TYPEFunc( TYPE_t* );

libhandle.c:

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <strings.h> #include "libTYPE.h" typedef struct { /* State variables go here. */ } TYPEimpl_t; extern size_t TYPESizeOf() { return sizeof( TYPEimpl_t ); } extern TYPE_t* TYPENew() { void* impl = calloc( 1, TYPESizeOf() ); TYPE_t* v = TYPEConstruct( impl ); return v; } extern TYPE_t* TYPEConstruct( void* mem ) { TYPEimpl_t* t = ( TYPEimpl_t* )mem; /* Construction code goes here. */ return ( TYPE_t* )t; } extern void TYPEDestruct( TYPE_t* t ) { TYPEimpl_t* timpl; if ( !t ) { return; } /* Destruction code goes here. */ timpl = ( TYPEimpl_t* )t; } extern void TYPEDelete( TYPE_t** t ) { if ( !t && !*t ) { return; } TYPEDestruct( *t ); free( *t ); *t = ( TYPE_t* )NULL; } extern int TYPEFunc( TYPE_t* t ) { TYPEimpl_t* timpl = ( TYPEimpl_t* )t; return 0; }

A sample Korn shell script below accepts a name of a custom data type as an argument, copies the Handle's template files into the new ones named after the given type and replaces the name TYPE with the given one.

mkhandle.sh:

#!/bin/ksh TP="$1" cat libhandle.h | sed "s/TYPE/"${TP}"/g" > lib${TP}.h cat libhandle.c | sed "s/TYPE/"${TP}"/g" > lib${TP}.c

Run this script as follows:

./mkhandle.sh str


Exercises

1) Implement the four popular container types - an array, a list, a binary tree and a hash table - as Handles.

2) A sample implementation of strToBytes() below returns a C array of unsigned characters which begins with sizeof(size_t) bytes containing the overall size of the array, followed by the contents of the string, followed by the null byte. The overall size of the returned array is equal to sizeof(size_t) plus the length of the string plus one for the null byte:

extern unsigned char* strToBytes( str_t* str, size_t* nb ) { unsigned char* bytes; strimpl_t* simpl = ( strimpl_t* )str; size_t ntslen = simpl->slen + sizeof( char ); *nb = sizeof( *nb ) + ntslen; bytes = malloc( *nb ); memcpy( bytes, nb, sizeof( *nb ) ); memcpy( bytes + sizeof( *nb ), simpl->buf, ntslen ); return bytes; }

Implement its counterpart strFromBytes():

extern str_t* strFromBytes( unsigned char* bytes, size_t nb ) { }

3) Write a sample program that:

- converts a string variable into an array of bytes
- converts the above array of bytes into a new string variable
- compares the original and the new string variables


Files

libstr.h libstr.c mklib.sh

str.c mkapp.sh

libhandle.h libhandle.c mkhandle.sh

\(\blacksquare\)

top prev next