A cross-platform and general purpose object model (cxom)

S.Rodriguez - July 18, 2004

Download the source code (23 kb)

 

Definition

What's the point of this article? Aren't C++ classes inherently cross-platform and able to describe and implement object models? If that is the case, then why should there be any discussion on this? Aren't Java, Python and other related cross-platform execution engines inherently cross-platform as well?

In short, the answers to the questions are yes to both, we have tools to solve common problems like this, and these solutions work as much they are dedicated and tailored to a particular problem. Yet, if you take C++, compiling enforces early binding, which leads to broken vtable issues (GPF most of the time). If you take Java, you've got a bunch of side effects like the size of the run-time, deployment issues related to the fact that those run-times need system credentials to host your code, versioning issues whenever that run-time gets upgraded, and plenty of others.

cxom's goal is to encourage the use of an alternative to all this. cxom is based on basic elements that make developer's lives easier and, in the end, customers happier with products that seem to work better, and come with better deployment experience for end users.

cxom heavily builds on the COM premise, keeps the best concepts, and gets rid of all Windows-only locks. It provides a cross-platform implementation for it, while COM is Windows-only, and does not rely on strongly typed .idl interfaces, even for remote machine method call scenarios.

 

Disclaimer

The source code is provided as is with no warranties of any kind.

 

Elements of cxom

The basic elements of cxom are as follows :

  • promotes late binding as first class citizen
  • enforces an homogeneous method call model
  • enforces a clean separation between interfaces and implementation
  • requires interfaces to be bare-minimum descriptions, acting like entry points to the underlying object model
  • crosses process boundaries
  • crosses machine boundaries

The above deserves a few explanations :

  • late binding : if early binding is not promoted instead, it's because of the implications of strong coupling with the service provider and the service consumption ; it's because of languages like C++ need to know all types being used in some method calling before anything gets compiled, yet if the source code compiles it may crash for unexpected reasons : the most obvious scenario is an interface which changed without notice, leading to vtable changes, thus requiring client code to be recompiled only to cope with the changes.
  • homogeneous method call model : this means that all method calls are expected to return a hresult, or error id (0 = no error), instead of returning one object type. Object types can be referenced in input parameters instead. This results in API without needless specifics in the returned value.
  • interface versus implementation : COM uses .idl-compiled C++ class interfaces (virtual pure method declarations) in order to enforce separation between the interfaces and everything else. This is good until you figure out this is not even a requirement for interfaces to declare every possible method declaration only to make a method call.
  • process boundaries : COM uses a different underlying marshalling mechanism depending on whether the target object model implementation is living in a DLL (inproc), a separate EXE (outproc) or an executable in some remote machine (DCOM). COM eventually uses sockets (MS knowledge base article), but hides this only to force Windows-only libraries to be used, thus preventing any portability of application code.
  • machine boundaries : sockets can be used either to cross process boundaries on the local machine, or between the local machine and some remote machine.

 

Constructing cxom

It begins with simple C function callbacks. After all, this many decade old mechanism is enough to provide dynamic method call execution at run-time, as in :

// declare a function pointer ; 
// note the signature of functions is hard-coded once for all
typedef int (*pfunc)(char* name);

// declare a structure to map functions with names
typedef struct _func
{
  char* funcname;
  pfunc funcptr;
} funcentry;


// implement functions
int func1(char* name)
{
  OutputDebugString("func1(");
  OutputDebugString(name);
  OutputDebugString(")\r\n");
  return 0;
}

int func2(char* name)
{
  OutputDebugString("func2(");
  OutputDebugString(name);
  OutputDebugString(")\r\n");
  return 0;
}

int func3(char* name)
{
  OutputDebugString("func3(");
  OutputDebugString(name);
  OutputDebugString(")\r\n");
  return 0;
}

// map names and functions
funcentry functables[] = {
  {"func1", func1},
  {"func2", func2},
  {"func3", func3},
  {"", NULL}
};

// function discovery helper
pfunc getfunc(char* funcname)
{
  int i = 0;
  while ( functables[i].funcptr )
  {
    if (stricmp(functables[i].funcname, funcname) == 0)
      return functables[i].funcptr;

    i++;
  }
  return NULL;
}

Once the initialization is done, calling one function dynamically is just a matter of using the functables map, as in :

// call function "func2" dynamically
(*getfunc("func2"))("hello world!");

This late method binding mechanism, apparently a bit more rude to read when expressed in C++ than higher-level languages, is at the heart of RAD especially all what has happened with VB, Delphi and other such languages since mid 90s. It's fine, but it's too poor a thing, that's why COM automation of tailored idl interfaces was added to developer tools in order to preserve some object hierarchy rather than a flat function model unlike the one above. And then we have that outproc and remote calling problem.

Even if the method binding is dynamic, the parameter binding is absolutely static since the client application has to comply with predefined signatures. That's yet another real limitation that COM automation addressed. In cxom, this is addressed as well.

What we want thus is a dynamic way to call methods and pass those parameters whose binding is not known statically by the client application. Static parameter binding is the common limitation of usual C++ interfaces (classes with virtual pure method declarations). In cxom, this is addressed by the variable ... mechanism used in functions like printf for instance.

The object hierarchy has to be reflected somehow. A default code pattern for that is to create proxies, ie interfaces that reflect the underlying object model. For instance, if the underlying model exposes a typical document model in which we find a MyDocuments collection, hosting one or more MyDocument objects, which in turn hosts one or more MySection objects, which in turn hosts textual content, then we must provide client applications a mechanism to reflect the hierarchy as well. This is done by exposing virtually empty interfaces IDocuments, IDocument and ISection. In the remainder of this document, the document model is used to exemplify cxom.

Unlike C++ interfaces, the IDocuments interface shall not expose a public method directly with parameters, like OpenDocument(char* documentname); to avoid any form of static parameter binding. Instead, the aim of interfaces is to proxy dynamic method calls, just like the COM IDispatch Invoke method.


object model exposure on the client side

On the "server" side, it's implemented like this :


object model implementation on the server side

 

On the client side thus, interfaces like IDocuments are passed, with as little knowledge of the underlying object types as possible, and the client side can execute method calls with the following :

IDocuments docs = ... ; // obtain the IDocuments interface
docs.MethodCall("Open","file.doc");

MethodCall is a method with a variable number of parameters, which is automatically defined thanks to the following macro :


// cxom.h //////////////////////////////////////////////////////////////////

#define CXOM_DEFAULT(type) \
	typedef (type::*t_m)(va_list* argList); \
	_HashTable<t_m> m_htVptrs;

#define CXOM_METHOD_GENERICDECL(type) \
	int MethodCall(char* methodname, ...) \
	{ \
		typedef (type::*tt_m)(va_list* argList); \
		tt_m pfunc = m_htVptrs.Lookup(methodname,NULL); \
		if (!pfunc) \
			return 0; \
		 \
		va_list argList; \
		va_start(argList, methodname); \
		 \
		return (this->*pfunc)(&argList); \
		\
		va_end(argList); \
	}


#define CXOM_METHOD(methodname) \
	int methodname(va_list* argList);


// object model ////////////////////////////////////////////////////////////

class IDocuments : public IBase
{
public:
  IDocuments();

  CXOM_DEFAULT(IDocuments);
  CXOM_METHOD_GENERICDECL(IDocuments);
  CXOM_METHOD(Open);
};

If this isn't obvious, any interface declares and implements a single and general MethodCall(char* methodname, ...) method whose goal is to redirect the incoming call to the appropriate internal method. The internal method in turn ("Open" in the example above) performs the actual indirection to the associated MyDocuments object, and its "Open" implementation. Of course, a key element is the parameters are passed dynamically along, regardless the amount and the types.

In order to perform the indirection, a hash table is created for each interface, and the implementer on the server side is responsible for registering each actual internal method implementation against it, as in :

#define CXOM_METHOD_REGISTER(methodname, methodvptr) \
  m_htVptrs.Add( #methodname , methodvptr);

IDocuments::IDocuments()
{
  CXOM_METHOD_REGISTER(Open,&IDocuments::Open);
}

Below is an example of implementation of the actual internal Open method implementation. Of course, it's just an example, and the client side is not aware of anything being done in it. When Open is called, the first parameter is the document name to open, and the second parameter is a pointer aimed to store the docment instance once it is opened :

int IDocuments::Open(va_list* argList)
{
  _ASSERT( m_cpBase != NULL);
  _ASSERT( argList != NULL);

  // grab the first parameter being passed
  char* p = va_arg(*argList,char*);
  if (!m_cpBase || !p)
    return 0;

  typedef MyDocument** LPLPMyDocument;

  // grab the second parameter
  LPLPMyDocument ppdoc = va_arg(*argList,LPLPMyDocument);
  if (!ppdoc)
    return E_INVALIDARG;

  MyDocuments* docs = (MyDocuments*) m_cpBase;
  *ppdoc = docs->OpenDocument(p);

  return S_OK; // return error code (0 = OK)
}

The only missing piece of the puzzle is the IBase interface. This class simply stores a pointer which maps the Ixxx interface to the actual xxx object implementation. The IBase interface is defined in cxom.h like this :

class IBase
{
protected:
  void* m_cpBase;
public:
  IBase()
  {
    m_cpBase = NULL;
  }
  void SetBase(void* pBase)
  {
    m_cpBase = pBase;
  }
  void* GetBase()
  {
    return m_cpBase;
  }
};

If you download the source code, you'll find that interfaces also declare indexers. In fact, this is entirely related to the semantics of the object model, not something mandatory. The indexers provide helper methods to access collection items and is particularly useful and natural for objects supposed to expose collection of documents and collection of sections.

 

From cxom to a general purpose method call mechanism

The current cxom implementation as of date (July 18, 2004) does not yet implement method calls across outside processes. This is however one of the main features of COM, where the method call mechanism is known as "out-proc method marshalling".

When it gets implemented, it's the CXOM_METHOD_GENERICDECL(...) that will be changed, makes use of sockets. Since this code is built-in in both sides, it is available to client applications. In addition, since all the socket stuff is hidden behind macros, it remains completely transparent to the client.

The socket mechanism applies to remote method calls as well, that's why there is so much hope behind it.

 

Code history

  • 0.1 (July 18, 2004) : first release ; simple inproc implementation

 

Feel free to post comments (here) to the article or to the source code.

 

 

Stephane Rodriguez.

Home
Blog