A cross-platform and general purpose object model (cxom)S.Rodriguez - July 18, 2004Download the source code (23 kb)
DefinitionWhat'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.
DisclaimerThe source code is provided as is with no warranties of any kind.
Elements of cxomThe basic elements of cxom are as follows :
The above deserves a few explanations :
Constructing cxomIt 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 // 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 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 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");
// 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 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 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 mechanismThe 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 The socket mechanism applies to remote method calls as well, that's why there is so much hope behind it.
Code history
Feel free to post comments (here) to the article or to the source code.
Stephane Rodriguez. |
Home Blog |