Creating a message processing or output node in C

A message processing node is used to process a message in some way, and an output node is used to produce a message as a bit stream.

Before you begin

About this task

When you code a message processing node or an output node, the nodes provide essentially the same services. You can perform message processing in an output node, and you can send a message to a bit stream by using a message processing node. For simplicity, this topic refers mainly to the node as a message processing node but it does also contain information about the functions of both types of node.

A loadable implementation library (LIL), is the implementation module for a C node. A LIL is implemented as a shared or dynamic link library (DLL), but has the file extension .lil not .dll.

For more information about the C node implementation functions that you write for the node, see C node implementation functions. You can call C node utility functions, implemented in the runtime integration node, to help with the node operation; see C node utility functions.

IBM® App Connect Enterprise provides the source for two sample user-defined nodes called SwitchNode and TransformNode. You can use these nodes in their current state, or you can modify them.

Declaring and defining your node

About this task

To declare and define a user-defined node to the integration node, include an initialization function, bipGetMessageflowNodeFactory, in your LIL. The following steps take place on the configuration thread and outline how the integration node calls your initialization function and how your initialization function declares and defines the user-defined node:

Procedure

  1. The integration node calls the initialization function bipGetMessageflowNodeFactory after the operating system has loaded and initialized the LIL. The integration node calls this function to understand what your LIL can do and how the integration node can call the LIL. For example:
    CciFactory LilFactoryExportPrefix * LilFactoryExportSuffix
    bipGetMessageflowNodeFactory()
    
  2. The bipGetMessageflowNodeFactory function must call the utility function cniCreateNodeFactory. This function passes back a factory name (or group name) for all the nodes that your LIL supports. The factory name (or group name) must be unique throughout all the LILs in a single runtime integration node.
  3. The LIL must call the utility function cniDefineNodeClass to pass the unique name of each node and a virtual function table of the addresses of the implementation functions.
    For example, the following code declares and defines a single node called MessageProcessingxNode:
    {
    	CciFactory* factoryObject;
    	int rc = 0;
    	CciChar factoryName[] = L"MyNodeFactory";
    	CCI_EXCEPTION_ST exception_st;
    
    	/* Create the Node Factory for this node */
    	factoryObject = cniCreateNodeFactory(0, factoryName);
    	if (factoryObject == CCI_NULL_ADDR) {
    		/* Any local error handling can go here */
    	}
    	else {
    		/* Define the nodes supported by this factory */
    	static CNI_VFT vftable = {CNI_VFT_DEFAULT};
    
    	/* Setup function table with pointers to node implementation functions */
    	vftable.iFpCreateNodeContext = _createNodeContext;
    	vftable.iFpDeleteNodeContext = _deleteNodeContext;
    	vftable.iFpGetAttributeName2 = _getAttributeName2;
    	vftable.iFpSetAttribute      = _setAttribute;
    	vftable.iFpGetAttribute2     = _getAttribute2;
    	vftable.iFpEvaluate          = _evaluate;
    
    	cniDefineNodeClass(0, factoryObject, L"MessageProcessingxNode", &vftable);
    
    	}
    
    	/* Return address of this factory object to the integration node */
    	return(factoryObject);
    }

    A user-defined node identifies itself as a message processing or output node by implementing the cniEvaluate function. User-defined nodes must implement either a cniEvaluate or a cniRun implementation function, otherwise the integration node does not load the user-defined node, and the cniDefineNodeClass utility function fails, returning CCI_MISSING_IMPL_FUNCTION.

    When a message flow containing a user-defined message processing node is deployed successfully, the node's cniEvaluate function is called for each message propagated to the node.

    Message flow data is received at the input terminal of the node, that is, the message, Environment, LocalEnvironment, and ExceptionList.

    For example:
    void cniEvaluate(                
      CciContext* context,                
      CciMessage* localEnvironment,        
      CciMessage* exceptionList,          
      CciMessage* message                 
    ){                                    
      ...
    }
    For the minimum code required to compile a C user-defined node, see C skeleton code.

Creating an instance of the node

About this task

To instantiate your node:

Procedure

  1. When the integration node has received the table of function pointers, it calls the function cniCreateNodeContext for each instantiation of the user-defined node. For example, if three message flows are using your user-defined node, your cniCreateNodeContext function is called for each of them. This function allocates memory for that instantiation of the user-defined node to hold the values for the configured attributes. For example:
    1. The user function cniCreateNodeContext is called:
      CciContext* _Switch_createNodeContext(
        CciFactory* factoryObject,
        CciChar*    nodeName,
        CciNode*    nodeObject
      ){
        static char* functionName = (char *)"_Switch_createNodeContext()";
        NODE_CONTEXT_ST* p;
        CciChar          buffer[256];
      
      
      
    2. Allocate a pointer to the local context and clear the context area:
        p = (NODE_CONTEXT_ST *)malloc(sizeof(NODE_CONTEXT_ST));
      
        if (p) {
           memset(p, 0, sizeof(NODE_CONTEXT_ST));
      
    3. Save the node object pointer in the context:
         p->nodeObject = nodeObject;
      
    4. Save the node name:
       CciCharNCpy((CciChar*)&p->nodeName, nodeName, MAX_NODE_NAME_LEN);
      
    5. Return the node context:
      return (CciContext*) p;
      
  2. The integration node calls the appropriate utility functions to find out about the node's input terminals and output terminals. A node has a number of input terminals and output terminals associated with it. Within the user function cniCreateNodeContext, calls must be made to cniCreateInputTerminal and cniCreateOutputTerminal to define the user node's terminals. These functions must be started within the cniCreateNodeContext implementation function.
    For example, to define a node with one input terminal and two output terminals:
        {
          const CciChar* ucsIn = CciString("in", BIP_DEF_COMP_CCSID) ;
          insInputTerminalListEntry(p, (CciChar*)ucsIn);
          free((void *)ucsIn) ;
        }
        {
          const CciChar* ucsOut = CciString("out", BIP_DEF_COMP_CCSID) ;
          insOutputTerminalListEntry(p, (CciChar*)ucsOut);
          free((void *)ucsOut) ;
        }
        {
          const CciChar* ucsFailure = CciString("failure", BIP_DEF_COMP_CCSID) ;
          insOutputTerminalListEntry(p, (CciChar*)ucsFailure);
          free((void *)ucsFailure) ;
        }

    The previous code starts the insInputTerminalListEntry and insOutputTerminalListEntry functions. You can find these functions in the sample code Common.c; see Sample node files. These functions define the terminals to the integration node and store handles to the terminals. Handles are stored in the structure referenced by the value returned in CciContext*. The node can then access the terminal handles from within the other implementation functions (for example CciEvaluate) because CciContext is passed to those implementation functions.

    The following code shows the definition of insInputTerminalListEntry:

    TERMINAL_LIST_ENTRY *insInputTerminalListEntry( 
      NODE_CONTEXT_ST* context, 
      CciChar*         terminalName 
    ){ 
      static char* functionName = (char *)"insInputTerminalListEntry()"; 
      TERMINAL_LIST_ENTRY* entry; 
      int                  rc; 
     
      entry = (TERMINAL_LIST_ENTRY *)malloc(sizeof(TERMINAL_LIST_ENTRY)); 
      if (entry) { 
     
        /* This entry is the current end of the list */ 
        entry->next = 0; 
     
        /* Store the terminal name */ 
        CciCharCpy(entry->name, terminalName); 
     
        /* Create terminal and save its handle */ 
        entry->handle = cniCreateInputTerminal(&rc, context->nodeObject, (CciChar*)terminalName); 
     
        /* Link an existing previous element to this one */ 
        if (context->inputTerminalListPrevious) context->inputTerminalListPrevious->next = entry; 
        else if ((context->inputTerminalListHead) == 0) context->inputTerminalListHead = entry; 
     
        /* Save the pointer to the previous element */ 
        context->inputTerminalListPrevious = entry; 
      } 
      else { 
        /* Error: Unable to allocate memory */ 
      } 
     
      return(entry); 
    } 
    

    The following example shows the code for insOutputTerminalListEntry:

    TERMINAL_LIST_ENTRY *insOutputTerminalListEntry( 
      NODE_CONTEXT_ST* context, 
      CciChar*         terminalName 
    ){ 
      static char* functionName = (char *)"insOutputTerminalListEntry()"; 
      TERMINAL_LIST_ENTRY* entry; 
      int                  rc; 
     
      entry = (TERMINAL_LIST_ENTRY *)malloc(sizeof(TERMINAL_LIST_ENTRY)); 
      if (entry) { 
     
        /* This entry is the current end of the list */ 
        entry->next = 0; 
     
        /* Store the terminal name */ 
        CciCharCpy(entry->name, terminalName); 
     
        /* Create terminal and save its handle */ 
        entry->handle = cniCreateOutputTerminal(&rc, context->nodeObject, (CciChar*)terminalName); 
     
        /* Link an existing previous element to this one */ 
        if (context->outputTerminalListPrevious) context->outputTerminalListPrevious->next = entry; 
        else if ((context->outputTerminalListHead) == 0) context->outputTerminalListHead = entry; 
     
        /* Save the pointer to the previous element */ 
        context->outputTerminalListPrevious = entry; 
      } 
      else { 
        /* Error: Unable to allocate memory */ 
      } 
     
      return(entry); 
    } 
    For the minimum code required to compile a C user-defined node, see C skeleton code.

Setting attributes

About this task

Attributes are set whenever you start the integration node, or when you redeploy a message flow with new values. Attributes are set by the integration node calling user code on the configuration thread. Your code needs to store these attributes in its node context area, for later use when processing messages.

Following the creation of input and output terminals, the integration node calls the cniSetAttribute function to pass the values for the configured attributes for this instantiation of the user-defined node. For example:
    {
      const CciChar* ucsAttr = CciString("nodeTraceSetting", BIP_DEF_COMP_CCSID) ;
      insAttrTblEntry(p, (CciChar*)ucsAttr, CNI_TYPE_INTEGER);
      _setAttribute(p, (CciChar*)ucsAttr, (CciChar*)constZero);
      free((void *)ucsAttr) ;
    }
    {
      const CciChar* ucsAttr = CciString("nodeTraceOutfile", BIP_DEF_COMP_CCSID) ;
      insAttrTblEntry(p, (CciChar*)ucsAttr, CNI_TYPE_STRING);
      _setAttribute(p, (CciChar*)ucsAttr, (CciChar*)constSwitchTraceLocation);
      free((void *)ucsAttr) ;
    }
The number of configuration attributes that a node can have is unlimited. However, a node must not implement an attribute that is already implemented as a base configuration attribute. The following list shows base attributes:
  • label
  • userTraceLevel
  • traceLevel
  • userTraceFilter
  • traceFilter

Implementing the node functionality

About this task

When the integration node retrieves a message from the queue, and that message arrives at the input terminal of your user-defined message processing or output node, the integration node calls the implementation function cniEvaluate. This function is called on the message processing thread and it must decide what to do with the message. This function might be called on multiple threads, especially if additional instances are used.

Deleting an instance of the node

About this task

If a node is deleted, the integration node calls the cniDeleteNodeContext function. This function is started on the same thread as cniCreateNodeContext. Use this function to release resources used by your user-defined node. For example:

void _deleteNodeContext(
  CciContext* context
){
  static char* functionName = (char *)"_deleteNodeContext()";
  free ((void*) context);
  return;
}