Apple Developer Connection
Advanced Search
Member Login Log In | Not a Member? Contact ADC

Writing Open
            Directory Plug-ins

Open Directory allows Mac OS X applications to locate and update information about users, machines, and resources on a network. Applications that make use of this directory service may provide transparent access to resources with a minimum of user intervention. For example, your application may use Open Directory in order to participate in a network-wide sign-on mechanism shared with Windows and UNIX users. Or your application may allow users to update their personal information in a company-wide directory of employee data. Adding Open Directory support to your application can create a lot of opportunities for you in the enterprise, and even in smaller network environments.

This article discusses the implementation of an Open Directory plug-in, which responds to requests for information about resources. This article also discusses an Open Directory client application that calls the plug-in. Note: you should have a second boot disk or CD available while testing. Since an Open Directory plug-in loads during system startup, any errors encountered may prevent the machine from fully booting, in which case you may not be able to login and remove the plug-in. Booting from another disk will allow you to remove the plug-in and then restart.

What Is Open Directory?

Originally NetInfo, a NeXT technology, provided the implementation for directory services on Mac OS X v10.0. NetInfo is still used for looking up directory information on a local machine, but other cross platform technologies have taken precedence in Apple's network directory system. Open Directory supports Lightweight Directory Access protocol (LDAP) versions 2 and 3, Apple's Bonjour protocol, and Microsoft's Active Directory, which allows Mac OS X machines to fit into a Microsoft network environment. Other supported legacy protocols include AppleTalk, BSD flat files, NIS, and the Service Location Protocol (SLP).

In this article you will see references to both Directory Services and Open Directory. While Open Directory provides the architecture for the directory system, Directory Services is the name of the physical framework (DirectoryService.framework) provided by Apple. Plus, the sample code uses a "DS" prefix for projects, files, and classes.

Plug-in Functions

Each protocol supported by Open Directory is implemented as a Core Foundation plug-in (type CFPlugIn). Three groups of functions are needed for a plug-in: those declared by the IUnknown interface, the Open Directory plug-in entry points, and the Open Directory callbacks. Apple provides the DSPlugIn Sample Code as part of the Directory Services SDK that includes an Xcode project and source files. The discussion of files in this section refers to the files in the C version of that project. You may elect to use the C++ version instead. The concepts discussed below remain the same.

The first set of functions is declared by the IUnknown interface. IUnknown is part of Microsoft's Component Object Model (COM). CFPlugin uses COM as the basis for function discovery and dispatch. IUnknown serves as the root interface for CFBundle's discovery and dispatching mechanism. IUnknown does not actually refer to functions that are unknown; instead it is used to perform a lookup of named functions in objects. (The book Inside COM, listed in the Additional Information section below, will tell you much more about COM internals.) IUnknown declares the functions QueryInterface, AddRef, and Release. An IUnknown implementation, typically defined in a structure, will include a reserved pointer (for use by the operating system) at the beginning followed by the three functions. ServerModule.c contains implementations of these functions, though with slightly different names (such as _COMQueryInterface). The names do not have to match because function dispatching or calling is handled through a function table. This table contains an array of pointers to function implementations (addresses). When a function gets called by name, that name is first translated to an index in the array, and the corresponding function pointer or address is then invoked.

The Open Directory plug-in interface declares the second set of functions, the entry points, or functions in your plug-in that may be called by Open Directory: Validate, Initialize, ProcessRequest, SetPlugInState, PeriodicTask, Shutdown, and Configure. These will be invoked to handle plug-in startup, execution of requests, and shutdown. The document Open Directory Plug-ins contains the API reference information.

The third set of functions consists of the Open Directory callbacks. Your plug-in calls these in order to interact with Open Directory. The callbacks include: DSRegisterNode, DSUnregisterNode, and DSDebugLog. The first two allow your plug-in to add a node to or remove a node from the set of global Open Directory entries. The last provides a way to add a message to the Open Directory log file, which may be viewed using Console.app. These callbacks are declared in ServerModuleLib.h and implemented in SerModuleLib.c.

The function implementations dispatch through to the corresponding function table implementations. The callbacks are located near the end of the function table, following the IUnknown functions and the plug-in entry points. Listing 1 contains the function table declaration from ServerModule.h in the sample code. The macro IUNKNOWN_C_GUTS expands to the three function declarations from IUnknown.

Listing 1: The Function Table Declaration in ServerModule.h

//-----------------------------------------------------------------------------
//	* Plugin Module Function Table representation for CFPlugin
//-----------------------------------------------------------------------------
// Function table for the com.apple.DSServer.ModuleInterface

typedef struct tagModuleInterfaceFtbl
{
    /**** Required COM header info. ****/
    IUNKNOWN_C_GUTS;

    /**** Instance methods. ****/
    long    (*validate)        ( void *thisp, const char *inVersionStr, 
                                 const unsigned long inSignature );
    long    (*initialize)      ( void *thisp );
    long    (*configure)       ( void *thisp );
    long    (*processRequest)  ( void *thisp, void *inData );
    long    (*setPluginState)  ( void *thisp, const unsigned long inState );
    long    (*periodicTask)    ( void *thisp );
    long    (*shutdown)        ( void *thisp );
    void    (*linkLibFtbl)     ( void *thisp, SvrLibFtbl *inLinkBack );

    unsigned long              mRefCount;
} ModuleFtbl;

The Plug-in Shell

Apple provides sample projects in the Directory Services SDK that simplifies development for both Open Directory plug-ins and clients. Figure 1 shows the plug-in shell Xcode project.

Xcode plug-in project screenshot

Figure 1: DSPlugIn Xcode Project

The recommended deployment directory for your plug-in is /Library/DirectoryServices/PlugIns/. After you build the DSPlugIn target, drag or copy the updated DSPlugIn.dsplug bundle from the project's /build folder to /Library/DirectoryServices/PlugIns/. You can test subsequent plug-in changes by deleting the existing /Library/DirectoryServices/PlugIns/DSPlugIn.dsplug, copying in the new bundle, then stopping the DirectoryService daemon using Activity Monitor. The next invocation of the client application (the sample project named DSTestTool) will restart the daemon, which will load the updated plug-in bundle. This sequence is easier than rebooting your machine each time you need to test the plug-in.

PlugInShell.c contains the plug-in entry points: Validate, Initialize, ProcessRequest, SetPlugInState, PeriodicTask, Shutdown, and Configure. This file is where your plug-in functionality goes. The other files provide support for interacting with the CFPlugIn architecture.

You should leave most of the property list entries as-is, though you may want to rev the CFBundleVersion as you add functionality.

Listing 2 includes the syslog header file, and declares several variables at the top of PlugInShell.c. These are used to keep track of the plug-in state, node name, and so on. They are referenced in the other plug-in functions discussed here.

Listing 2: Additional Include File and Global Variables Declared in PlugInShell.c

#include <syslog.h>

static ePluginState        gPluginState   = kUnknownState;
static unsigned long       gSignature     = 0;
static tDataListPtr        gNodeList      = NULL;
static tDirReference       gDSRef         = NULL;
const  char *              nodePath       = "/DSPlugIn/local";

Listing 3 shows the plug-in's Initialize function. It calls CreateNode and AddTestRecords. Note the inclusion of syslog messages; this will be discussed in more detail in the section on debug messages.

Listing 3: The Initialize Function

long PlugInShell_Initialize ( void )
{
	LogIt( 0x0F, "DSPlugInStub %s Initialize Called %s\n", "method", "now" );

	syslog( LOG_INFO, "PlugInShell_Initialize" );

	// Need to register nodes in some loop or other logic

	AddTestRecords();

	long status = CreateNode();

	return( eDSNoErr );
	
} // Initialize

Listing 4 shows the structure, global variables, and function supporting simple test records for this plug-in. This makes it easy to test interaction with the client application later.

Listing 4: Creating Sample Records

typedef struct {
    char recordName[ 32 ];
    char attrName[ 32 ];
    char attrValue[ 32 ];
} DSPlugInRecordStructure;

static DSPlugInRecordStructure gRecords[ 10 ];

static unsigned long gRecordCount = 0;

void AddTestRecords ( void )
{
    gRecordCount = 0;
    
    strcpy( gRecords[ gRecordCount ].recordName, "test" );
    strcpy( gRecords[ gRecordCount ].attrName, "message" );
    strcpy( gRecords[ gRecordCount ].attrValue, "Hello, world." );
    
    gRecordCount++;
    
    strcpy( gRecords[ gRecordCount ].recordName, "another record" );
    strcpy( gRecords[ gRecordCount ].attrName, "message" );
    strcpy( gRecords[ gRecordCount ].attrValue, "Hello, world (again)." );
    
    gRecordCount++;
         
    syslog( LOG_INFO, "PlugInShell.AddTestRecord" );
}

Initialize also handles node registration, discussed next.

Adding and Removing a Node

Clients lookup services by node, rather than by requesting a specific plug-in. Listing 5 illustrates how to add or register a node. Once the node has been registered, clients that know to look for it (by name) can start making requests. Open Directory will locate the appropriate plug-in based on the requested node; clients do not need to know the name of the plug-in.

Listing 5: Adding a Node

long CreateNode ( void )
{
    long status = eDSNoErr;     
         
    syslog( LOG_INFO, "PlugInShell.CreateNode" );

    if ( gDSRef == NULL )
        status = dsOpenDirService( &gDSRef );

    syslog( LOG_INFO, "PlugInShell.CreateNode: dsOpenDirService returned %ld", status );

    if ( gDSRef != NULL )
    {
        syslog( LOG_INFO, "PlugInShell.CreateNode: gDSRef != NULL" );

        if ( gNodeList == NULL )
        {   
            gNodeList = dsDataListAllocate( gDSRef );
            status = dsBuildListFromPathAlloc ( gDSRef, gNodeList, nodePath, "/" );         

            syslog( LOG_INFO, "PlugInShell.CreateNode: gNodeList = %p", gNodeList );
            syslog( LOG_INFO, "PlugInShell.CreateNode: dsBuildListFromPathAlloc returned = %d", status );

            if ( gNodeList != NULL )    
            {
                status = DSRegisterNode( gSignature, gNodeList, kDirNodeType );

                syslog( LOG_INFO, "PlugInShell.CreateNode: DSRegisterNode returned %ld", status );
            }
            else
            {
                syslog( LOG_INFO, "\t*** PlugInShell.CreateNode: gNodeList == NULL" );
            }
        }
    }

    return status; 
}

Listing 6 illustrates node removal. This does not unload the plug-in, it simply makes the node inaccessible to clients. This function is called from Shutdown.

Listing 6: Removing a Node

long RemoveNode ( void )
{
    long status = eDSNoErr;     
    
    syslog( LOG_INFO, "PlugInShell.RemoveNode" );

    if ( gNodeList != NULL )
    {
        status = DSUnregisterNode( gSignature, gNodeList );
    }
    
    return status;
}

Processing Requests

Plug-ins must respond to a variety of requests, as outlined in the Open Directory Plug-Ins manual. These include dsOpenDirNode, dsCreateRecord, and so on. The plug-in entry point for these requests is the ProcessRequest function. The client invokes a particular function, and the parameters provided by the caller are packaged into an appropriate structure type. The sHeader type, shown in Listing 7, contains two fields that are common to the other structure types. This allows the plug-in to cast the incoming data to an sHeader, determine the requested operation using the fType field, and then dispatch to an appropriate block of code to handle the request. Inside the handler block, the plug-in casts the incoming data to a structure type that is specific to that operation. For a dsGetRecordList request, the appropriate type is sGetRecordList.

Listing 7: The Generic Header and Record List Structures

typedef struct {
    unsigned long           fType;
    long                    fResult;
} sHeader;

typedef struct {
    unsigned long           fType;
    long                    fResult;
    tDirNodeReference       fInNodeRef;
    tDataBufferPtr          fInDataBuff;
    tDataListPtr            fInRecNameList;
    tDirPatternMatch        fInPatternMatch;
    tDataListPtr            fInRecTypeList;
    tDataListPtr            fInAttribTypeList;
    dsBool                  fInAttribInfoOnly;
    unsigned long           fOutRecEntryCount;
    tContextData            fIOContinueData;
} sGetRecordList;

Once the plug-in casts the function parameter to an sGetRecordList pointer, it fills-in the response field, fInDataBuff. Listing 8 implements dsGetRecordList. Note that this plug-in ignores the Boolean field fInAttribInfoOnly, which specifies whether the plug-in should return attribute names and values (a value of false) or names only (true). This plug-in returns both names and attributes.

The client is responsible for allocating memory for several of the fields in the sRecordList, while the plug-in allocates others. In either case, deallocation is the client's responsibility. Check the API documentation for details.

The record list format may be of two predefined types, StdA or StdB, or you may define your own type. This example uses StdA. The formats are defined in the Open Directory Plug-ins manual. The advantage of using one of the predefined types is that the system will automatically attempt to interpret the record list: a request for a specific record out of the tDataBufferPtr will not call your plug-in to parse the record list. This is an example of Client Side Buffer Parsing, which significantly improves performance. If you use a custom format for the returned data, the message traffic between the client and your plug-in increases, and your plug-in must respond to additional operations, such as dsGetRecordEntry.

Listing 8: Building the Record List in ProcessRequest

long PlugInShell_ProcessRequest ( void *inData )
{
    long                 siresult      = 0;
    sHeader              *pMsgHdr      = nil;
    tDataBufferPtr       theDataBufferPtr = nil;

    unsigned long offset = 0;

    syslog( LOG_INFO, "PlugInShell_ProcessRequest" );

    if ( inData == nil )
    {
        return( -4460 );
    }

    pMsgHdr = ( sHeader * )inData;

    syslog( LOG_INFO, "PlugInShell_ProcessRequest: pMsgHdr->fType = %d", pMsgHdr->fType );

    switch ( pMsgHdr->fType )
    {
        // snip...
        
        case kGetRecordList:
            syslog( LOG_INFO, "PlugInShell_ProcessRequest: kGetRecordList" );

            sGetRecordList *getRecordListData = ( sGetRecordList * )inData;
            theDataBufferPtr = ( tDataBufferPtr )getRecordListData->fInDataBuff;

            // Used to memcpy numeric values.
            unsigned long scratch = 0;
            unsigned short shortScratch = 0;
            
            // Current location in buffer.
            offset = 0;

            // General scheme for building the buffer, repeated continuously:
            //   1. Write next value.
            //     a. Use memcpy for ulong or ushort values.
            //     b. Use strcpy for strings. Add 1 to all strlen results for terminator.
            //   2. Update the buffer location.
            scratch = 'StdA';
            memcpy( theDataBufferPtr->fBufferData + offset, &scratch, sizeof( scratch ) );
            offset += 4;

            scratch = gRecordCount;
            memcpy( theDataBufferPtr->fBufferData + offset, &scratch, sizeof( scratch ) );
            offset += 4;

            // Skip past record offset values for now.
            // Fill them in as each record gets processed.
            offset += ( 4 * gRecordCount );

            scratch = 'EndT';
            memcpy( theDataBufferPtr->fBufferData + offset, &scratch, sizeof( scratch ) );
            offset += 4;
            
            // Count down. Greatest index will be first after the StdA data block.
            unsigned long i = gRecordCount - 1;
            unsigned long recordOffset = offset;

            while ( i >= 0 )
            { 
                // Set length of record block.
                // Use a unique type because this is a non-standard record format.
                // Note addition of 1 for each strlen call.
                // The 10 is the number of bytes needed for the fixed-length fields.
                scratch = strlen( "dsRecTypeNative:MyType" ) + 1 + 
                    strlen( gRecords[ i ].recordName ) + 1 + 10;
                memcpy( theDataBufferPtr->fBufferData + offset, &scratch, sizeof( scratch ) );
                offset += 4;
                
                // Set record type length.
                shortScratch = strlen( "dsRecTypeNative:MyType" ) + 1;
                memcpy( theDataBufferPtr->fBufferData + offset, &shortScratch, sizeof( shortScratch ) );
                offset += 2;
                
                // Set record type.
                strcpy( theDataBufferPtr->fBufferData + offset, "dsRecTypeNative:MyType" );
                offset += strlen( "dsRecTypeNative:MyType" ) + 1;
                
                // Set record name length.
                shortScratch = strlen( gRecords[ i ].recordName ) + 1;
                memcpy( theDataBufferPtr->fBufferData + offset, &shortScratch, sizeof( shortScratch ) );
                offset += 2;
                
                // Set record name.
                strcpy( theDataBufferPtr->fBufferData + offset, gRecords[ i ].recordName );
                offset += strlen( gRecords[ i ].recordName ) + 1;
                
                // Set number of attributes.
                shortScratch = 1;
                memcpy( theDataBufferPtr->fBufferData + offset, &shortScratch, sizeof( shortScratch ) );
                offset += 2;

                // Set attribute length: name plus value lengths, plus size of fixed-length fields.
                scratch = strlen( gRecords[ i ].attrName ) + 1 + strlen( gRecords[ i ].attrValue ) + 1 + 8;
                memcpy( theDataBufferPtr->fBufferData + offset, &scratch, sizeof( scratch ) );
                offset += 4;

                // Set attribute name length.
                shortScratch = strlen( gRecords[ i ].attrName ) + 1;
                memcpy( theDataBufferPtr->fBufferData + offset, &shortScratch, sizeof( shortScratch ) );
                offset += 2;
                
                // Set attribute name.
                strcpy( theDataBufferPtr->fBufferData + offset, gRecords[ i ].attrName );
                offset += strlen( gRecords[ i ].attrName ) + 1;
                
                // Set number of attribute values.
                shortScratch = 1;
                memcpy( theDataBufferPtr->fBufferData + offset, &shortScratch, sizeof( shortScratch ) );
                offset += 2;
                
                // Set attribute value length.
                scratch = strlen( gRecords[ i ].attrValue ) + 1;
                memcpy( theDataBufferPtr->fBufferData + offset, &scratch, sizeof( scratch ) );
                offset += 4;
                
                // Set attribute value.
                strcpy( theDataBufferPtr->fBufferData + offset, gRecords[ i ].attrValue );
                offset += strlen( gRecords[ i ].attrValue ) + 1;

                // Adjust length of entire structure.
                theDataBufferPtr->fBufferLength = offset;
                
                // Set offset to this record, way back near the beginning of the StdA structure.
                scratch = recordOffset;
                memcpy( theDataBufferPtr->fBufferData + 8 + ( 4 * i ), &scratch, sizeof( scratch ) );
                
                // Set length of this record, at the beginning of the record.
                scratch = offset - recordOffset;
                memcpy( theDataBufferPtr->fBufferData + recordOffset, &scratch, sizeof( scratch ) );
                
                // Starting location for next record.
                recordOffset = offset;

                // Adjust record count.
                getRecordListData->fOutRecEntryCount++;
                
                // Ignore this because we know we have enough space for returning everything.
                // Should check total length and make sure everything fit, else set this field
                // and break.
                getRecordListData->fIOContinueData = NULL;
                
                // Handle countdown for unsigned value.
                if ( i > 0 )
                    i--;
                else
                    break;
            }

            siResult = eDSNoErr;//eNotYetImplemented;
            
            syslog( LOG_INFO, "PlugInShell_ProcessRequest: kGetRecordList: returning..." );

            break;

Custom Calls

A plug-in may respond to custom requests that are not defined by Open Directory. Custom requests are also handled in ProcessRequest. The data structure passed for a dsDoPlugInCustomCall request is shown in Listing 9.

Listing 9: Data Structure for dsDoPlugInCustomCall in PluginData.h

typedef struct {
    unsigned long           fType;
    long                    fResult;
    tDirNodeReference       fInNodeRef;
    unsigned long           fInRequestCode;
    tDataBufferPtr          fInRequestData;
    tDataBufferPtr          fOutRequestResponse;
} sDoPlugInCustomCall;

Listing 10 shows custom call handling in ProcessRequest. A message id of 0 returns a simple greeting, and a 2 changes the greeting string using data passed by the client.

Listing 10: Handling a Custom Call in ProcessRequest

long PlugInShell_ProcessRequest( void *inData )
{
    long                siResult        = 0;
    sHeader             *pMsgHdr        = nil;
    sDoPlugInCustomCall *customCallData = nil;

    if ( inData == nil )
    {
        return( -4460 );
    }

    pMsgHdr = (sHeader *)inData;

    switch ( pMsgHdr->fType )
    {
        // snip...

        case kDoPlugInCustomCall:
            customCallData = ( sDoPlugInCustomCall * )inData;

            if ( customCallData->fInRequestCode == 0 )
            {
                // Greeting.
                dsDataNodeSetLength( customCallData->fOutRequestResponse, strlen( gMessage ) );
                strcpy( customCallData->fOutRequestResponse->fBufferData, gMessage );
            }
            else if ( customCallData->fInRequestCode == 2 )
            {
                strcpy( gMessage, customCallData->fInRequestData->fBufferData );
            }
            
            siResult = eDSNoErr;
            break;
            
        default:
            siResult = eNotHandledByThisNode;
            break;
    }

    pMsgHdr->fResult = siResult;

    return( siResult );
}

Debug Messages

Debugging a plug-in during development can be difficult. The code in this article uses syslog to send messages to the console. It works reliably while Open Directory is starting. The callback DSDebugLog may cause the system to hang, and is not used in this plug-in.

Figure 2 shows the Console application's server log. These are high-level messages from Open Directory itself, primarily regarding the success or failure of each plug-in during startup.

Console application server log displaying plug-in load info

Figure 2: Open Directory Server Log

Figure 3 shows the Console application's system log, including numerous debugging messages generated by DSPlugIn using the syslog facility. One advantage of this approach is that Console.app retains logs between sessions. The syslog syntax is:

    syslog( <severity_level>, "<message>" );

The severity constants and the function are declared in syslog.h. The following example logs an informational message from DSPlugIn's Shutdown method. The timestamp, host, and process ID are automatically generated by the syslog facility.

    syslog( LOG_INFO, "PlugInShell_Shutdown" );
Debugging using syslog and the Console application system log

Figure 3: Debugging using syslog

Occasionally things go wrong during development. Figure 4 shows several thread call stacks from the Console application's crash log for Open Directory. The thread that crashed was executing dsBuildListFromStrings from within its Initialize function. Something bad happened while determining the node name string length.

Console application Open Directory crash log

Figure 4: Crash Log

The Client Application

The client application DSTestTool calls DSPlugIn and other Open Directory plug-ins. DSTestTool accepts option flags on the command-line. The main function parses the options, creates an instance of the DSTestTool class, and dispatches to other routines. In Listing 11, most of main has been removed except for initialization and flags that specify requests to DSPlugIn.

Listing 11: DSTestTool.main()

int main ( int argc, char * const *argv )
{
    int          i            = 0;
    int          nextParam    = 2;
    char         *p           = nil;
    char         *pNodeName   = nil;
    sInt32       siStatus     = eDSNoErr;
    DSTestTool   myClass;

    if ( argc > 1 )
    {
        p = strstr( argv[1], "-" );

        if ( p != nil )
        {
            // snip...

            siStatus = myClass.Initialize();
            if ( siStatus == noErr )
            {
                while ( *p != '\0' )
                {
                    switch ( *p )
                    {
                        // snip...

                        case 'u':
                            myClass.SetNodeCustomMessage();
                            break;

                        case 'w':
                            myClass.GetNodeCustomMessage();
                            break;

                        case 'z':
                            myClass.GetRecordList();
                            break;
                    }

                    p++;
                }

                myClass.Deinitialize();
            }
        }
        else
        {
            DoHelp( stderr, argv[0] );
            exit( 1 );
        }
    }
    else
    {
        DoHelp( stderr, argv[0] );
        exit( 1 );
    }
} // main

Listing 12 shows DSTestTool::GetNodeCustomMessage. This function calls dsDoPlugInCustomCall, passing a reference to the node, a request code, and in and out data buffers. The node reference allows Open Directory to call the correct plug-in. The request code is a programmer-defined unsigned long that allows you to specify, for example, what type of custom processing the plug-in should perform. The input buffer allows you to pass data to the plug-in, and the output buffer contains the results from the plug-in. In this case there is no input to send. The plug-in simply fills the output buffer.

Listing 12: Calling dsDoPlugInCustomCall from DSTestTool

sInt32 DSTestTool::GetNodeCustomMessage( void )
{
    sInt32             status        = eDSNoErr;
    char               *nodePath     = "/DSPlugIn/local";
    tDirNodeReference  nodeRef       = nil;
    tDataListPtr       inRequest     = nil;
    
    status = OpenDirNode( nodePath, &nodeRef );
        
    tDataBufferPtr outData = dsDataBufferAllocate( fDSRef, 1024 );    
    tDataBufferPtr inData = dsDataBufferAllocate( fDSRef, 1024 );    
    status = dsDoPlugInCustomCall( nodeRef, 0, inData, outData );
    fprintf( stdout, "dsDoPlugInCustomCall returned %s of length %ld\n",
        outData->fBufferData, outData->fBufferLength );
    status = dsDataBufferDeAllocate( fDSRef, inData );
    status = dsDataBufferDeAllocate( fDSRef, outData );
         
    status = CloseDirectoryNode( nodeRef );
    
    return( status );
}

Figure 5 shows the results of invoking DSTestTool with the -w and -z flags. The -w flag requests the plug-in's message string, and -z requests the DSPlugIn's records.

Figure 5: Invoking DSTestTool

$ ./dstesttool -w
dsDoPlugInCustomCall returned Hello from DSPlugIn! of length 20

$ ./dstesttool -z
    Record Name     = test
      1 - 1: (message) Hello, world.
    Record Name     = another record
      1 - 1: (message) Hello, world (again).
        recCount = 2

$

Configuring a Plug-In

Figure 6 shows the Directory Access utility and loaded plug-ins. The Checkbox indicates whether a plug-in has a configuration application available, as defined by the CFBundleConfigAvail entry in the plug-in's property list (Info.plist). If you set this property incorrectly or do not make the configuration application available in the correct location, Directory Access will display an error dialog when you select the plug-in and click Configure. But it will not crash. If you want to create a configuration app that executes in the context of Directory Access, contact Apple Developer Relations for details.

Directory Access utility showing plug-ins

Figure 6: Available Plug-ins in Directory Access

Alternatively, a standalone configuration application is the simplest way to configure a plug-in. Once the app locates the plug-in, it can make a custom request, as discussed earlier in this article. Listing 13 contains a function in DSTestTool that invokes dsDoPlugInCustomCall in the plug-in and passes a (hardcoded) new greeting string. You can add a user interface or command-line switch that allows the user or administrator to specify a custom value for the greeting, as well as other parameters.

Listing 13: Changing the Plug-In Greeting from DSTestTool

sInt32 DSTestTool::GetNodeCustomMessage( void )
{
    sInt32             status        = eDSNoErr;
    char               *nodePath     = "/DSPlugIn/local";
    tDirNodeReference  nodeRef       = nil;
    
    status = OpenDirNode( nodePath, &nodeRef );
        
    tDataBufferPtr outData = dsDataBufferAllocate( fDSRef, 1024 );    
    tDataBufferPtr inData = dsDataBufferAllocate( fDSRef, 1024 );    
    strcpy( inData->fBufferData, "New message!" );
    dsDataNodeSetLength( inData, strlen( "New message!" ) ); 
    status = dsDoPlugInCustomCall( nodeRef, 2, inData, outData );
    status = dsDataBufferDeAllocate( fDSRef, inData );
    status = dsDataBufferDeAllocate( fDSRef, outData );
         
    status = CloseDirectoryNode( nodeRef );
    
    return( status );
}

Read This Before Deploying Your Plug-In

The plug-in discussed in this article is not a complete implementation. It fills-in some blanks in the sample code, but you need to implement the remaining functions or at least make sure they return eNotYetImplemented while still under development. A more useful plug-in will need to access resources in the system or on the network. This leads to two critical issues that a developer needs to resolve before sending a plug-in to users:

  1. Your plug-in runs as a trusted system daemon, so make sure it is bulletproof. Although Open Directory is resilient, and will automatically restart after a crash, bad code can continue to bring it down and kill system performance.
  2. Your plug-in MUST perform authorization before accepting requests to write shared data. You might also consider authorizing requests for data reads. Use dsDoDirNodeAuth to perform authorization. Authorization protects the system from rogue requests to change critical data, such as user records. If your plug-in allows write access to system and user data, but does not check the credentials of the user making the request, that plug-in represents a serious security risk.

For More Information

Using the techniques discussed in this article, you can create Open Directory plug-ins that respond to requests for information from Open Directory clients. The references below contain additional details on these topics.

  • Join or search the darwin development mailing list.
  • The document Open Directory overview discusses how to work with nodes and records, and other client tasks. It also contains Open Directory reference material.
  • The document Open Directory plug-in overview discusses the Open Directory runtime architecture, required plug-in entry points, callbacks into the Open Directory service, and how to configure a plug-in. It also contains reference material for the plug-in API.
  • The CFPlugin internals are based on Microsoft's Component Object Model (COM), and the book Inside COM by Dale Rogers, published by Microsoft Press, provides the technical details about this technology.
  • Joe Zobkiw's book, Mac OS X Advanced Programming Techniques (Sams Publishing) discusses the COM side of CFPlugin from the Mac OS X perspective. This book also includes a Carbon plug-in, and even though it is not an Open Directory plug-in, many of the concepts still apply.
  • Cocoa Documentation on Loading Resources
  • Core Foundation documentation on PlugIns

Aside from the sample code, you may wish to check out the following frameworks and header files:

DirectoryService.framework

  • DirServices.h, DirServicesConst.h, and DirServicesTypes.h declare the functions, constants, and date types, respectively, for this framework.
  • DirServicesUtils.h declares functions for allocating, populating, and deallocating various Open Directory data structures, such as tDataNode, tDataList, and tRecordEntry.

CoreFoundation.framework

  • CFPlugInCOM.h contains Apple's definitions of the COM types, constants, and macros. The "non-Apple" style of macro names and types reflects COM's Windows heritage.
  • CFPlugIn.h contains definitions for various utility and dispatch functions used by plug-ins.

Updated: 2004-09-20