4.2 Enabling very late binding

Very late binding makes it possible to access objects without having compile-time knowledge of them. This is especially useful for script hosts, but also for bridging solutions. In the context of component technology, bridging solutions make it possible for components written for one component model to be usable with code written for another. Like a script host, a bridge does not have compile-time knowledge of all interfaces it communicates with on behalf of other code, and as such needs to use very late binding.

In this chapter, very late binding is realized through late binding. The new interface that enables dynamically validated calls, Scriptable, is implemented by all classes that wish to be accessible from clients such as scripts. A script interpreter or a bridge has compile-time knowledge of this interface, and funnels all calls through it using late binding. It is up to the implementing class to check the validity of an incoming call (whether the given operation exists and the given arguments are of the correct types), call the proper operation (using late binding), and finally interpret and return the return value, if any.

The C header file of the Scriptable interface is displayed in Listing 4.5. It includes ScriptableReturnValue_t for return values, which relies on ScriptableArgumentType_t, shown in Listing 4.6.

Listing 4.5: Scriptable.h
/**
 * @file
 *
 * This file contains an interface, <code>Scriptable</code>, which
 * allows invocations to be checked at runtime. In other words, it
 * facilitates very late binding. With late binding, the
 * implementations of interfaces are found at runtime, but invocations
 * are checked statically. An object implementing this interface may
 * be called from environments that do not allow invocations to be
 * checked statically, such as scripting languages.
 */

#ifndef INCLUSION_GUARD_SCRIPTABLE
#define INCLUSION_GUARD_SCRIPTABLE

#include <stdbool.h>
#include <wchar.h>
#include "Fundamental.h"
#include "ScriptableArgumentType.h"

/**
 * The name of this interface.
 */
#define SCRIPTABLE_NAME (L"se.polberger.components.Scriptable")

/* Convenience macros for the operations inherited from the
 * Fundamental interface:
 */
#define Scriptable_SwitchInterface(pScriptable,                         \
                                   pInterfaceName,                      \
                                   ppResult)                            \
  (pScriptable)->pDispatchTable->SwitchInterface(pScriptable,           \
                                                 pInterfaceName,        \
                                                 ppResult);
#define Scriptable_AddReference(pScriptable)                            \
  if (pScriptable != NULL)                                              \
  {                                                                     \
    (pScriptable)->pDispatchTable->AddReference(pScriptable);           \
  }
#define Scriptable_RemoveReference(pScriptable)                         \
  if (pScriptable != NULL)                                              \
  {                                                                     \
    (pScriptable)->pDispatchTable->RemoveReference(pScriptable);        \
  }

// Convenience macros for the operations defined in this interface:

/**
 * Invokes the specified operation with the given arguments. This
 * operation will fail if the underlying implementation does not
 * support the given operation, or if the argument list is not
 * correct. All arguments must be of the correct types.
 *
 * The <code>firstArgumentType</code> must be set to the type of the
 * first argument, or
 * <code>ScriptableArgumentType_NO_MORE_ARGUMENTS</code> if the called
 * operation accepts no arguments. It is followed by the data of the
 * first argument. If there are additional arguments, the next
 * argument must be set to the type of the next argument, followed by
 * its data, and so on. The last given argument to this operation must
 * be <code>ScriptableArgumentType_NO_MORE_ARGUMENTS</code>.
 *
 * @param[in] pScriptable
 *   the instance implementing this interface. Must not be
 *   <code>NULL</code>.
 * @param[in] pOperationName
 *   the name of the operation. This string is case-sensitive. Must
 *   not be <code>NULL</code.>
 * @param[out] pReturnValue
 *   a pointer to the variable which shall hold the return value
 *   returned from the called operation. This variable is normally
 *   allocated on the stack. If the return value holds a reference to
 *   an object implementing the <code>Scriptable</code> interface
 *   (that is, if <code>pReturnValue->type ==
 *   ScriptableArgumentType_SCRIPTABLE</code>), the caller is
 *   responsible for removing the reference that has been added.
 * @param[in] firstArgumentType
 *   the type of the first argument. This must be set to
 *   <code>ScriptableArgumentType_NO_MORE_ARGUMENTS</code> if the
 *   called operation does not accept any arguments.
 * @return
 *   <code>true</code> if the operation completes successfully,
 *   <code>false</code> otherwise.
 */
#define Scriptable_InvokeOperation(pScriptable,                         \
                                   pOperationName,                      \
                                   pReturnValue,                        \
                                   ...)                                 \
  (pScriptable)->pDispatchTable->InvokeOperation(pScriptable,           \
                                                 pOperationName,        \
                                                 pReturnValue,          \
                                                 __VA_ARGS__)

struct Scriptable_DispatchTable_s;

/**
 * Variables of this type may be used to represent objects
 * implementing this interface.
 */
typedef struct
{
  const struct Scriptable_DispatchTable_s* pDispatchTable;
} Scriptable_t;

struct ScriptableReturnValue_s;

/**
 * This is the dispatch table, or vtable, for this interface. It
 * represents an indirection that enables the implementation of an
 * interface to be bound to at runtime. Interfaces wishing to extend
 * this interface must include the complete contents of this structure
 * as the first members of their dispatch tables, changing the
 * <code>Scriptable_t</code> type to match their own. (Only single
 * interface inheritance is supported.)
 */
typedef struct Scriptable_DispatchTable_s
{
  // Operations inherited from the Fundamental interface:
  bool (*SwitchInterface)(Scriptable_t* pThis,
                          wchar_t* pInterfaceName,
                          Fundamental_t** ppResult);
  void (*AddReference)(Scriptable_t* pThis);
  void (*RemoveReference)(Scriptable_t* pThis);

  // Operations defined in the Scriptable interface:
  bool (*InvokeOperation)(Scriptable_t* pThis,
                          wchar_t* pOperationName,
                          struct ScriptableReturnValue_s* pReturnValue,
                          ScriptableArgumentType_t firstArgumentType,
                          ...);
} Scriptable_DispatchTable_t;

/**
 * This type represents a return value from a scriptable operation. It
 * is a tagged union, that is, a structure which contains the type of
 * its data, as well as a union representing the data.
 */
typedef struct ScriptableReturnValue_s
{
  /**
   * The type of the data stored in this tagged union (its
   * <em>tag</em>).
   */
  ScriptableArgumentType_t type;

  union
  {
    /**
     * Data corresponding to
     * <code>ScriptableArgumentType_INTEGER</code>.
     */
    unsigned int integerValue;

    /**
     * Data corresponding to
     * <code>ScriptableArgumentType_DOUBLE</code>.
     */
    double doubleValue;

    /**
     * Data corresponding to
     * <code>ScriptableArgumentType_BOOLEAN</code>.
     */
    bool booleanValue;

    /**
     * Data corresponding to
     * <code>ScriptableArgumentType_CHARACTER</code>.
     */
    wchar_t characterValue;

    /**
     * Data corresponding to
     * <code>ScriptableArgumentType_SCRIPTABLE</code>. If the return
     * value contains data of this type, the reference must be removed.
     * at some point.
     */
    Scriptable_t* pScriptableValue;
  } values;
} ScriptableReturnValue_t;

#endif // INCLUSION_GUARD_SCRIPTABLE

Listing 4.6: ScriptableArgumentType.h
/**
 * @file
 *
 * This file contains an enumerated type representing the different
 * types that may be passed to (and returned from) the
 * <code>Scriptable::InvokeOperation()</code> operation.
 */

#ifndef INCLUSION_GUARD_SCRIPTABLE_ARGUMENT_TYPE
#define INCLUSION_GUARD_SCRIPTABLE_ARGUMENT_TYPE

/**
 * This enumerated type represents the different types that may be
 * passed to (and returned from) the
 * <code>Scriptable::InvokeOperation()</code> operation.
 */
typedef enum
{
  /**
   * The type is invalid.
   */
  ScriptableArgumentType_INVALID,

  /**
   * There is no type. This is used for non-existent return values,
   * and may not be used for arguments to an operation.
   */
  ScriptableArgumentType_VOID,

  /**
   * The type represents an unsigned integer (an <code>unsigned
   * int</code>).
   */
  ScriptableArgumentType_INTEGER,

  /**
   * The type represents a double-precision floating-point value
   * conforming to IEEE 754 (a <code>double</code>).
   */
  ScriptableArgumentType_DOUBLE,

  /**
   * The type represents a boolean value (a <code>bool</code>).
   */
  ScriptableArgumentType_BOOLEAN,

  /**
   * The type represents a character (a <code>wchar_t</code>).
   */
  ScriptableArgumentType_CHARACTER,

  /**
   * The type represents an arbitrary object which implements the
   * <code>Scriptable</code> interface.
   */
  ScriptableArgumentType_SCRIPTABLE,

  /**
   * This enumerator signifies that no more arguments are expected in
   * a call to <code>Scriptable::InvokeOperation()</code>.
   */
  ScriptableArgumentType_NO_MORE_ARGUMENTS
} ScriptableArgumentType_t;

#endif // INCLUSION_GUARD_SCRIPTABLE_ARGUMENT_TYPE

Scriptable has only one operation, the “meta operation” InvokeOperation(). A client calls this operation to invoke a named operation offered by the class. It passes the name of the operation to invoke, followed by a pointer to a (preferably stack-allocated) tagged union which is to hold the return value after the commencement of the call, and finally the arguments that are to be passed to the invoked operation. The arguments are given in pairs: the first member of the pair denotes the type of the argument, and the second member the actual data. ScriptableArgumentType_NO_MORE_ARGUMENTS must be the last argument. Listing 4.7 shows an excerpt from a program testing late and very late binding by calling Node::PrintDebugInformation() using both invocation mechanisms.

Listing 4.7: Excerpt from NodeTest.c
  if (result)
  {
    printf("\n");
    printf("Tree (using late binding):\n");
    result = Node_PrintDebugInformation(pRootNode, 0, 2);
  }

  if (result)
  {
    printf("\n");
    printf("Tree (using very late binding):\n");

    Scriptable_t* pScriptableRootNode = NULL;
    ScriptableReturnValue_t returnValue;

    result =
      Scriptable_SwitchInterface(pRootNode,
                                 SCRIPTABLE_NAME,
                                 (Fundamental_t**)&pScriptableRootNode);

    if (result)
    {
      result = Scriptable_InvokeOperation(
        pScriptableRootNode,
        L"PrintDebugInformation",
        &returnValue,
        ScriptableArgumentType_INTEGER,
        0,
        ScriptableArgumentType_INTEGER,
        2,
        ScriptableArgumentType_NO_MORE_ARGUMENTS);
    }

    if (result)
    {
      result = (returnValue.type == ScriptableArgumentType_VOID);
    }

    /* While we don't expect the return type to be of type Scriptable,
     * we are contractually obligated to remove the reference if this
     * type is returned.
     */
    if (returnValue.type == ScriptableArgumentType_SCRIPTABLE)
    {
      Scriptable_RemoveReference(returnValue.values.pScriptableValue);
    }

    Scriptable_RemoveReference(pScriptableRootNode);

The supported types, for both input arguments and return values, are unsigned integers, double-precision floating point values, boolean values, wide characters, as well as arbitrary objects implementing the Scriptable interface. There is no type for strings, as the most prudent way to represent a string is arguably as an object, a strategy which ensures that memory will be properly managed through reference counting. A new interface, String, could be introduced, and provided that classes implementing this interface also implement Scriptable, strings would be available through very late binding. (Though it would likely be more appropriate for a script host to have compile-time knowledge of the String interface and map native strings in the scripting language to the String interface using late binding.)

DefaultBinaryOperatorNode implements the new interface in the most straightforward way possible. Its performance is not optimal; using a data structure such as a hash table would greatly speed up the string comparisons. The implementation is shown in Listing 4.8.

Listing 4.8: Excerpt 3 from a revised version of DefaultBinaryOperatorNode.c
static bool DefaultBinaryOperatorNode_InvokeOperation(
  DefaultBinaryOperatorNode_InterfaceNode_t* pInterfaceNode,
  wchar_t* pOperationName,
  ScriptableReturnValue_t* pReturnValue,
  ScriptableArgumentType_t firstArgumentType,
  ...)
{
  bool result =
    (pInterfaceNode != NULL) &&
    (pOperationName != NULL) &&
    (pReturnValue != NULL) &&
    (firstArgumentType != ScriptableArgumentType_INVALID) &&
    (firstArgumentType != ScriptableArgumentType_VOID);
  BinaryOperatorNode_t* pBinaryOperatorNode =
    (BinaryOperatorNode_t*)
    &(pInterfaceNode->pInstanceData->interfaceNodeBinaryOperatorNode);

  va_list arguments;
  va_start(arguments, firstArgumentType);

  if (result)
  {
    if (wcscmp(pOperationName, SCRIPTABLE_OPERATION_IS_CONSTANT) == 0)
    {
      bool isConstant = false;

      // This operation takes no input arguments.
      result =
        (firstArgumentType == ScriptableArgumentType_NO_MORE_ARGUMENTS);

      if (result)
      {
        result = BinaryOperatorNode_IsConstant(pBinaryOperatorNode,
                                               &isConstant);
      }

      if (result)
      {
        pReturnValue->type = ScriptableArgumentType_BOOLEAN;
        pReturnValue->values.booleanValue = isConstant;
      }
    }
    else if (wcscmp(pOperationName,
                    SCRIPTABLE_OPERATION_PRINT_DEBUG_INFORMATION) == 0)
    {
      ScriptableArgumentType_t lastType = firstArgumentType;
      unsigned int argumentCount = 0;
      unsigned int startPosition = 0;
      unsigned int indentationSize = 0;

      // This operation takes two arguments, both integers.
      result = (lastType != ScriptableArgumentType_NO_MORE_ARGUMENTS);

      while (result &&
             (lastType != ScriptableArgumentType_NO_MORE_ARGUMENTS))
      {
        argumentCount++;
        result = argumentCount <= 2;

        if (result)
        {
          result = (lastType == ScriptableArgumentType_INTEGER);
        }

        if (result)
        {
          switch (argumentCount)
          {
            case 1:
              startPosition = va_arg(arguments, unsigned int);
              break;

            case 2:
              indentationSize = va_arg(arguments, unsigned int);
              break;

            default:
              result = false;
              break;
          }
        }

        if (result)
        {
          lastType = va_arg(arguments, ScriptableArgumentType_t);
        }
      }

      if (result)
      {
        result =
          BinaryOperatorNode_PrintDebugInformation(pBinaryOperatorNode,
                                                   startPosition,
                                                   indentationSize);
      }

      if (result)
      {
        pReturnValue->type = ScriptableArgumentType_VOID;
      }
    }
    else if (wcscmp(pOperationName, SCRIPTABLE_OPERATION_OPERATOR) == 0)
    {
      BinaryOperator_t operator = BinaryOperator_UNDEFINED;

      // This operation takes no input arguments.
      result =
	(firstArgumentType == ScriptableArgumentType_NO_MORE_ARGUMENTS);

      if (result)
      {
        result = BinaryOperatorNode_Operator(pBinaryOperatorNode,
                                             &operator);
      }

      if (result)
      {
        pReturnValue->type = ScriptableArgumentType_INTEGER;
        pReturnValue->values.integerValue = (unsigned int)operator;
      }
    }
    else if (wcscmp(pOperationName,
                    SCRIPTABLE_OPERATION_LEFT_OPERAND) == 0)
    {
      Node_t* pNode = NULL;
      Scriptable_t* pScriptableNode = NULL;

      // This operation takes no input arguments.
      result =
        (firstArgumentType == ScriptableArgumentType_NO_MORE_ARGUMENTS);

      if (result)
      {
        result = BinaryOperatorNode_LeftOperand(pBinaryOperatorNode,
                                                &pNode);
      }

      if (result)
      {
        /* The return value is an object accessed through the Node
         * interface. We can only return this value if the class
         * implementing this interface also supports the Scriptable
         * interface, and we thus need to test for this at runtime.
         */
        result = Node_SwitchInterface(pNode,
                                      SCRIPTABLE_NAME,
                                      (Fundamental_t**)&pScriptableNode);
      }

      if (result)
      {
        pReturnValue->type = ScriptableArgumentType_SCRIPTABLE;
        pReturnValue->values.pScriptableValue = pScriptableNode;
      }

      Node_RemoveReference(pNode);
    }
    else if (wcscmp(pOperationName,
                    SCRIPTABLE_OPERATION_RIGHT_OPERAND) == 0)
    {
      /* This code is very similar to the one for handling the left
       * operand, and has therefore been omitted from this code
       * listing.
       */
    }
    else
    {
      result = false;
    }
  }

  va_end(arguments);

  if ((!result) && (pReturnValue != NULL))
  {
    pReturnValue->type = ScriptableArgumentType_INVALID;
  }

  return result;
}