UNO C++ Bridges

From Apache OpenOffice Wiki
< Documentation‎ | DevGuide
Revision as of 07:12, 5 June 2008 by OOoWikiBot (talk | contribs) (Robot: Changing Category:Documentation/Developers Guide/Advanced UNO)
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