UNO C++ Bridges
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). |