UNO C++ Bridges

From Apache OpenOffice Wiki
< Documentation‎ | DevGuide
Revision as of 11:59, 1 November 2007 by Jsc (talk | contribs) (Initial author Sun Microsystems, Inc.)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search




This chapter focuses on writing a UNO bridge locally, specifically writing a C++ UNO bridge to connect to code compiled with the C++ compiler. This is an introduction for bridge implementers.. It is assumed that the reader has a general understanding of compilers and a of 80x86 assembly language. Refer to the section Implementation Loader for additional information.

Binary UNO Interfaces

A primary goal when using a new compiler is to adjust the C++-UNO data type generator (cppumaker tool) to produce binary compatible declarations for the target language. The tested cppu core functions can be used when there are similar sizes and alignment of UNO data types. The layout of C++ data types, as well as implementing C++-UNO objects is explained in C++ Language Binding.

When writing C++ UNO objects, you are implementing UNO interfaces by inheriting from pure virtual C++ classes, that is, the generated cppumaker classes (see .hdl files). When you provide an interface, you are providing a pure virtual class pointer. The following paragraph describes how the memory layout of a C++ object looks.

A C++-UNO interface pointer is always a pointer to a virtual function table (vftable), that is, a C++ this pointer. The equivalent binary UNO interface is a pointer to a struct _uno_Interface that contains function pointers. This struct holds a function pointer to a uno_DispatchMethod() and also a function pointer to acquire() and release():

 // forward declaration of uno_DispatchMethod()
 
 typedef void (SAL_CALL * uno_DispatchMethod)(
          struct _uno_Interface * pUnoI, 
          const struct _typelib_TypeDescription * pMemberType,
          void * pReturn,
          void * pArgs[],
          uno_Any ** ppException );
 
 // Binary UNO interface
 
 typedef struct _uno_Interface 
 {
          /** Acquires uno interface.
 
              @param pInterface uno interface
  
           */
          void (SAL_CALL * acquire )( struct _uno_Interface * pInterface );
          /** Releases uno interface.
 
           @param pInterface uno interface
           */
          void (SAL_CALL * release )( struct _uno_Interface * pInterface );
          /** dispatch function
           */
          uno_DispatchMethod pDispatcher ;
 } uno_Interface;

Similar to com.sun.star.uno.XInterface, the life-cycle of an interface is controlled using the acquire() and release() functions of the binary UNO interface. Any other method is called through the dispatch function pointer pDispatcher. The dispatch function expects the binary UNO interface pointer (this), the interface member type of the function to be called, an optional pointer for a return value, the argument list and finally a pointer to signal an exception has occurred.

The caller of the dispatch function provides memory for the return value and the exception holder (uno_Any).

The pArgs array provides pointers to binary UNO values, for example, a pointer to an interface reference (_uno_Interface **) or a pointer to a SAL 32 bit integer (sal_Int32 *).

A bridge to binary UNO maps interfaces from C++ to binary UNO and conversely. To achieve this, implement a mechanism to produce proxy interfaces for both ends of the bridge.

C++ Proxy

A C++ interface proxy carries its interface type (reflection), as well as its destination binary UNO interface (this pointer). The proxy's vftable pointer is patched to a generated vftable that is capable of determining the index that was called ,as well as the this pointer of the proxy object to get the interface type.

The vftable requires an assembly code. The rest is programmed in C/C++. You are not allowed to trash the registers. On many compilers, the this pointer and parameters are provided through stack space. The following provides an example of a Visual C++ 80x86:

 vftable slot0:
 mov eax, 0
 jmp cpp_vftable_call
 vftable slot0:
 mov eax, 1
 jmp cpp_vftable_call
 vftable slot0:
 mov eax, 2
 jmp cpp_vftable_call
 ...
 
 static __declspec(naked) void __cdecl cpp_vftable_call(void)
 {
 __asm
           {
                   sub              esp, 8           // space for immediate return type
                   push             esp
                   push             eax              // vtable index
                   mov              eax, esp
                   add              eax, 16
                   push             eax              // original stack ptr
                   call             cpp_mediate      // proceed in C/C++
                   add              esp, 12
                   // depending on return value, fill registers
                   cmp              eax, typelib_TypeClass_FLOAT
                   je               Lfloat
                   cmp              eax, typelib_TypeClass_DOUBLE
                   je               Ldouble
                   cmp              eax, typelib_TypeClass_HYPER
                   je               Lhyper
                   cmp              eax, typelib_TypeClass_UNSIGNED_HYPER
                   je               Lhyper
                   // rest is eax
                   pop              eax
                   add              esp, 4
                   ret
 Lhyper:           pop              eax
                   pop              edx
                   ret
 Lfloat:           fld              dword ptr [esp]
                   add              esp, 8
                   ret
 Ldouble:          fld              qword ptr [esp]
                   add              esp, 8
                   ret
           }
 }

The vftable is filled with pointers to the different slot code (snippets). The snippet code recognizes the table index being called and calls cpp_vftable_call(). That function calls a C/C++ function (cpp_mediate()) and sets output registers upon return, for example, for floating point numbers depending on the return value type.

Remember that the vftable handling described above follows the Microsoft calling convention, that is, the this pointer is always the first parameter on the stack. This is currently not the case for gcc that prepends a pointer to a complex return value before the this pointer on the stack if a method returns a struct. This complicates the (static) vftable treatment, because different vftable slots have to be generated for different interface types, adjusting the offset to the proxy this pointer:

Microsoft Visual C++ call stack layout (esp offset [byte]):
0: return address
4: this pointer
8: optional pointer, if return value is complex (i.e. struct to be copy-constructed)
12: param0
16: param1
20: ...

This is usually the hardest part for stack-oriented compilers. Afterwards proceed in C/C++ (cpp_mediate()) to examine the proxy interface type, read out parameters from the stack and prepare the call on the binary UNO destination interface.

Each parameter is read from the stack and converted into binary UNO. Use cppu core functions if you have adjusted the cppumaker code generation (alignment, sizes) to the binary UNO layout (see cppu/inc/uno/data.h).

After calling the destination uno_dispatch() method, convert any out/inout and return the values back to C++-UNO, and return to the caller. If an exception is signalled (*ppException != 0), throw the exception provided to you in ppException. In most cases, you can utilize Runtime Type Information (RTTI) from your compiler framework to throw exceptions in a generic manner. Disassemble code throwing a C++ exception, and observe what the compiler generates.

Binary UNO Proxy

The proxy code is simple for binary UNO. Convert any in/inout parameters to C++-UNO values, preparing a call stack. Then perform a virtual function call that is similar to the following example for Microsoft Visual C++:

 void callVirtualMethod(
           void * pThis, sal_Int32 nVtableIndex,
           void * pRegisterReturn, typelib_TypeClass eReturnTypeClass,
           sal_Int32 * pStackLongs, sal_Int32 nStackLongs )
 {
           // parameter list is mixed list of * and values
           // reference parameters are pointers
 
 __asm
           {
                   mov              eax, nStackLongs
                   test             eax, eax
                   je               Lcall
                   // copy values
                   mov              ecx, eax
                   shl              eax, 2                // sizeof(sal_Int32) == 4
                   add              eax, pStackLongs      // params stack space
 Lcopy:            sub              eax, 4
                   push             dword ptr [eax]
                   dec              ecx
                   jne              Lcopy
 Lcall:
                   // call
                   mov              ecx, pThis
                   push             ecx   // this ptr
                   mov              edx, [ecx]  // pvft
                   mov              eax, nVtableIndex
                   shl              eax, 2                // sizeof(void *) == 4
                   add              edx, eax
                   call             [edx]                 // interface method call must be __cdecl!!!
 
                   // register return
                   mov              ecx, eReturnTypeClass
                   cmp              ecx, typelib_TypeClass_VOID
                   je               Lcleanup
                   mov              ebx, pRegisterReturn
 // int32
                   cmp              ecx, typelib_TypeClass_LONG
                   je               Lint32
                   cmp              ecx, typelib_TypeClass_UNSIGNED_LONG
                   je               Lint32
                   cmp              ecx, typelib_TypeClass_ENUM
                   je               Lint32
 // int8
                   cmp              ecx, typelib_TypeClass_BOOLEAN
                   je               Lint8
                   cmp              ecx, typelib_TypeClass_BYTE
                   je               Lint8
 // int16
                   cmp              ecx, typelib_TypeClass_CHAR
                   je               Lint16
                   cmp              ecx, typelib_TypeClass_SHORT
                   je               Lint16
                   cmp              ecx, typelib_TypeClass_UNSIGNED_SHORT
                   je               Lint16
 // float
                   cmp              ecx, typelib_TypeClass_FLOAT
                   je               Lfloat
 // double
                   cmp              ecx, typelib_TypeClass_DOUBLE
                   je               Ldouble
 // int64
                   cmp              ecx, typelib_TypeClass_HYPER
                   je               Lint64
                   cmp              ecx, typelib_TypeClass_UNSIGNED_HYPER
                   je               Lint64
                   jmp              Lcleanup                  // no simple type
 Lint8:
                   mov              byte ptr [ebx], al
                   jmp              Lcleanup
 Lint16:
                   mov              word ptr [ebx], ax
                   jmp              Lcleanup
 Lfloat:
                   fstp             dword ptr [ebx]
                   jmp              Lcleanup
 Ldouble:
                   fstp            qword ptr [ebx]
                   jmp             Lcleanup
 Lint64:
                   mov             dword ptr [ebx], eax
                   mov             dword ptr [ebx+4], edx
                   jmp             Lcleanup
 Lint32:
                   mov             dword ptr [ebx], eax
                   jmp             Lcleanup
 Lcleanup:
                   // cleanup stack
                   mov             eax, nStackLongs
                   shl             eax, 2                    // sizeof(sal_Int32) == 4
                   add             eax, 4                    // this ptr
                   add             esp, eax
            }
 }

First stack data is pushed to the stack., including a this pointer, then the virtual function's pointer is retrieved and called. When the call returns, the return register values are copied back. It is also necessary to catch all exceptions generically and retrieve information about type and data of a thrown exception. In this case, look at your compiler framework functions also.

Additional Hints

Every local bridge is different, because of the compiler framework and code generation and register allocation. Before starting, look at your existing bridge code for the processor, compiler, and the platform in module bridges/source/cpp_uno that is part of the OpenOffice.org source tree on www.openoffice.org.

Also test your bridge code extensively and build the module cppu with debug symbols before implementing the bridge, because cppu contains alignment and size tests for the compiler.

For quick development, use the executable build in cppu/test raising your bridge library, doing lots of calls with all kinds of data on mapped interfaces.

Also test your bridge in a non-debug build. Often, bugs in assembly code only occur in non-debug versions, because of trashed registers. In most cases, optimized code allocates or uses more processor registers than non-optimized (debug) code.

Content on this page is licensed under the Public Documentation License (PDL).
Personal tools