UNO C++ Bridges
- Implementing UNO Language Bindings
- UNO C++ Bridges
- UNO Reflection API
- XInvocation Bridge
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 Apache OpenOffice 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). |