Developing BREW
Application Using Pure C++
Darklen
caesarshen@gmail.com
9.8.2005
- Developing
BREW Application Using Pure C++
- GOAL
- PROBLEMS
- Access member functions
- Cast between BREW Interfaces
- Declare BREW Extensions
- Instantiate BREW Extensions
- BREW Callbacks
- THE MEMORY LAYOUT OF BREW INTERFACES
- C++ EQUIVALENCE OF BREW INTERFACES
- DIFFERENT BETWEEN C FUNCTIONS AND
C++ MEMBER FUNCTIONS
- SOLUTIONS
- VISUAL C++ ISSUES
- ADV1.2 ARM C++ COMPILER ISSUES
- GNU ARM C++ ISSUES
- CONCLUSIONS
GOAL
The goal of this research is to find a solution to develop BREW
programs and extensions using pure C++ language without the lost of any
speed and space efficiency.
To achieve this, the resulting C++ program has to meet following
criteria:
- It is written in pure C++ language. BREW Extensions are also
declared in C++ class form, not QINTERFACE() macro.
- It treats BREW Interfaces as C++ objects.
- It consumes no more memory than the C implementation.
- It runs as fast as the C implementation.
PROBLEMS
QUALCOMM’s Binary Runtime Environment for Wireless® (BREW®)
follows an object-oriented design, which exposes its functionality
through BREW Interfaces. BREW Interfaces are conceptually equivalent to
the interfaces in many object-oriented languages such as java and C++,
which contains the definition of a set of member functions. BREW
Interfaces, like their counterparts in Java and C++, support
inheritance and polymorphism. However all these features are simulated
because the official programming language of BREW is C which is not
object-oriented. Due to this design, it is quite inconvenience to write
code to manipulate BREW Interfaces in our programs.
Access member
functions
BREW declares its Interfaces in C structure and provides a set of
MACROs to access their member functions. However, programmers who are
familiar with JAVA or C++ are more like to access them in an object
oriented way. For example, instead of use IBASE_Realse(pObj),
we’d like to use pObj->Release().
Cast between BREW
Interfaces
Polymorphism between BREW Interfaces is permitted by BREW. For example,
we can cast an instance of an interface to an instance of its super
interface. However, we have to cast it explicitly, for example:
IBase * pObj = (IBase*) pModule; // Assume pModule is an instance of
IModule which is a derived interface of IBase
|
The above code is not type safe and may cause problems when pModule is
not actually an instance of IModule. The C compiler doesn’t understand
inheritance and will not catch this error at compile time. It is better
if we can write code like this:
IBase * pObj = pModule; // OK, don’t have to cast since the
Compiler know that IModule derives from IBase.
IModule * pMod = pApplet; //
Compile time ERROR! IApplet does not derive from IModule, assume
pApplet is
//an instance of IApplet which does not derive from IModule
|
Declare BREW
Extensions
In order to write BREW extensions, we have to declare our own interface
like below:
#define INHERIT_IBase(iname) \
uint32
(*AddRef) (iname*);\
uint32
(*Release) (iname*)
QINTERFACE(IBase)
{
INHERIT_IBase(IBase);
};
QINTERFACE(IObject)
{
INHERIT_IBASE(IObject);
Uint32 (*GetHashCode)(IObject*
pThis);
};
#define
IOBJECT_AddRef(p)
GET_PVTBL(p,IObject)->AddRef(p)
#define
IOBJECT_Release(p)
GET_PVTBL(p,IObject)->Release(p)
#define
IOBJECT_GetHashCode(p)
GET_PVTBL(p,IObject)->CreateInstance(p) |
The above code is complicated and error-prone. To add a member
function, we have to declare both a function pointer in the Interface
and a macro that access it. It is better if we can write the equivalent
code like this:
class IBase{
public:
virtual uint32 AddRef() =0;
virtual uint32 Release()=0;
};
class IObject: public IBase{
public:
virtual uint32 GetHashCode()=0;
}; |
Instantiate BREW
Extensions
Creating instances of BREW Extensions includes following steps which
have to be written by hand:
- Allocate the memory for both the object and v-table.
- Fill the v-table with function pointers.
- Set the value of pvt to the address of the v-table.
- Initialize other data members
The following code, which is a part of AEEApplet_New.c, creates an
instance of IApplet:
Iboolean AEEApplet_New(int16 nIn,
AEECLSID clsID,
IShell * pIShell,
IModule * pIModule,
IApplet **ppobj,
AEEHANDLER pAppHandleEvent,
PFNFREEAPPDATA pFreeAppData)
{
AEEApplet * pme = NULL;
VTBL(IApplet) * appFuncs;
int
nSize;
if(nIn < 0)
return(FALSE);
nSize = (int)nIn;
if(!ppobj)
return(FALSE);
*ppobj = NULL;
//Validate parameters
if (!pIShell || !pIModule)
return FALSE;
if(nSize < sizeof(AEEApplet))
nSize += sizeof(AEEApplet);
// Create an Applet object
if (NULL == (pme = (AEEApplet*)MALLOC(nSize +
sizeof(IAppletVtbl))))
return FALSE;
appFuncs = (IAppletVtbl *)((byte *)pme + nSize);
//Initialize the individual entries in the VTBL
appFuncs->AddRef =
AEEApplet_AddRef;
appFuncs->Release =
AEEApplet_Release;
appFuncs->HandleEvent = AEEApplet_HandleEvent;
INIT_VTBL(pme, IApplet, *appFuncs); //
Initialize the VTBL
//Initialize the data members
pme->m_nRefs = 1;
pme->m_pIShell = pIShell;
pme->m_pIModule = pIModule;
pme->m_pIDisplay = NULL;
pme->clsID =
clsID;
//Store the function pointers to APP's HandleEvent and
FreeAppData functions
pme->pAppHandleEvent = pAppHandleEvent;
pme->pFreeAppData = pFreeAppData;
ISHELL_CreateInstance(pIShell, AEECLSID_DISPLAY,
(void **)&pme->m_pIDisplay);
if (!pme->m_pIDisplay) {
//Cleanup
FREE_VTBL(pme, IApplet);
FREE(pme);
return FALSE;
}
ISHELL_AddRef(pIShell);
IMODULE_AddRef(pIModule);
*ppobj = (IApplet*)pme;
return TRUE;
}
|
AEEApplet_New acts similar to the constructor of a C++ class but is
quite complicated; any mistakes in it may cause subtle bugs that are
hard to track. If we use C++, most code in AEEApplet_NEW() will
be generated by the compiler and we only have to write following code:
//NOTE: Error detection code is removed
for simplicity.
CMyApplet::CMyApplet(IShell* pIShell, IModule* pIModule, CLSID id)
: m_pIShell(pIShell),m_pModule(pModule),m_Id(id)
{
m_pIShell->AddRef();
m_pModule->AddRef();
m_pIShell->CreateInstance(AEECLSID_DISPLAY,
&m_pDisplay);
}
IApplet * myApp = new CMyApplet( pIShell, pIModule,CLSID )
|
BREW Callbacks
BREW APIs only accept callbacks as C function pointers, not the
pointers of C++ class member functions. In C++ program,s we have
to
write a wrapper function like this:
void time_wrapper(CApp * pThis)
{
pThis->DoMainLoop();
}
class CApp
{
public:
void
start()
{
ISHELL_SetTimer(m_pIShell, 1, (PFNNOTIFY)time_wrapper, this);
}
void
DoMainLoop()
{
//DO SOMETHING
}
}; |
The above code works fine but the wrapper function itself consumes some
CPU circle as it calls the actual member function. For
applications, such as Games, performance is extremely important. I have
seen many games have a huge main_loop function that contains a big
switch statement with 3000 lines of code; I understand the time wasted
on an extra function call is unacceptable for them.
THE MEMORY LAYOUT OF
BREW INTERFACES
When we declares a BREW Interface using the QINTERFACE() macro,
we actually declare two structures, the BREW Interface and its v-table.
For example, if we declare an interface IObject that derives from IBase
and has a single member function called GetHashCode, the following
structures are declared:
struct IObjectVtbl
{
IBaseVtbl parent;
Uint32 (*GetHashCode)(IObject* pThis);
};
struct IObject
{
IObjectVtbl* pvt; //When the instance is
initialized, this value points to a valid v-table.
}; |
The first element of instances of BREW Interface is always a pointer to
the v-table. The v-table is an array that contains the absolute address
of the interface’s member functions. In addition, if an interface is
derived from other interfaces, its v-table must contain all the
elements of the super interface’s v-table and its instance must contain
all the fields of the instance of the super interface. Figure A shows
the memory layout of an instance of the IBase Interface.
Figure A The memory layout of a BREW Interface
C++ EQUIVALENCE OF
BREW INTERFACES
For some experienced C++ programmer, they might have already found that
this memory layout is identical to the memory layout of a C++ object
generated by most modern C++. By this assumption, we can declare a C++
class whose instance has exactly the same memory layout as the BREW
Interface and cast an instance of the BREW Interface into an instance
of the C++ class.
For example, if we define CFont as bellow, we can safely cast IFont *
to CFont* and use the C++ way to access the member functions of IFont.
class CBase
{
public:
virtual uint32 AddRef()=0;
virtual uint32 Release()=0;
};
class CQueryInterface : public CBase
{
public:
virtual int
QueryInterface)(AEECLSID, void **)=0;
};
class CFont: public CQueryInterface
{
public:
virtual int DrawText (IBitmap *pDst, int x, int
y,
const AECHAR *pcText, int nChars,NativeColor fg,
NativeColor bg,
const AEERect *prcClip, uint32 dwFlags)=0;
virtual int MeasureText(IFont *pMe, const AECHAR
*pcText, int nChars,
int nMaxWidth, int *pnFits, int *pnPixels)=0;
virtual int GetInfo(AEEFontInfo *pinfo, int nSize)=0;
}; |
DIFFERENT BETWEEN C
FUNCTIONS AND C++ MEMBER FUNCTIONS
C++ member functions are identical to C functions except that they have
an implicit argument called ‘this’ which points to the object and is
the first parameter of the member function. If we declare a C++ member
function like this:
Class CMyObject
{
int FunctionA(int a, bool b);
}; |
It is equivalent to this C declaration:
| int FunctionA(CMyObject* this,
int a, bool b); |
We can cast a C++ member function into an ordinary C function and use
it as a callback if we handle the ‘this’ parameter correctly:
class CApp
{
void start_timer();
void do_timer();
};
int (* PFNCALLBACK)(void *);
int(CApp::*PFNMEMBER)();
void SetTimer(PFNCALLBACK pCallback, void* param) ;
PFNCALLBACK brutal_cast( PFNMEMBER pmember)
{
union
{
PFNCALLBACK m_callback;
PFNMEMBER m_member;
} a;
a.m_member = pmember;
return a.m_callback;
}
void CApp::start_timer()
{
// This
is safe,
because the first parameter of CApp::dotimer() is the ‘this’ pointer.
SetTimer(brutal_cast(&CApp::do_timer),this);
}
|
NOTE: brutal_cast() is
used to cast from a C++ member function to
a C function.
SOLUTIONS
Based on the fact that it is possible to declare C++ classes whose
instances have the same memory layout as the instances of BREW
Interfaces, we can re-declare BREW Interfaces into their C++
equivalences, and use the C++ equivalences instead of the BREW
Interfaces in our C++ programs. By using the C++ equivalences, we can
access BREW Interfaces as if they are C++ classes, and the C++ program
will not consume extra memory or CPU circles comparing to the C
implementation.
After understanding the differences between C++ member functions and C
functions, we can cast pointers of C++ member functions directly into
pointers of C member functions and always put the ‘this’ pointer as the
first parameter of the C function.
We have written some demo programs to verify that above techniques work
both on BREW emulators and real handsets. Also, the C++ programs are
considerably shorter than their C counterparts (1/2 of the code
length). Below is the statistics:
C++
brewpp.cpp(142 lines ), brewpp.h (147 lines)
main_app.cpp (123 lines)
(Total 412 lines)
C
AEEAppGen.c (325 lines ), AEEAppGen.h(106 lines ), AEEModGen.c
(400 lines), AEEModGen.h(109 lines)
main.c( 62 lines )
(Total 1002 lines)
VISUAL C++ ISSUES
There is no known issue in Visual C++ and any other windows desktop
compilers. Demo programs runs perfectly in the BREW emulator.
ADV1.2 ARM C++
COMPILER ISSUES
Unfortunately, these demo C++ programs will not run in the real handset
if they are compiled with the ADV1.2 ARM C++ Compiler. The reason
is that class instances in ARM C++ compiled programs do not use the
common memory layout that is compatible with the memory layout of the
BREW Interfaces. If we declare a C++ class CBase which is
equivalent to IBase:
class CBase
{
public:
virtual uint32 AddRef()=0;
virtual uint32 Release()=0;
};
|
And write following test code:
void test(IBase* b1, CBase* b2)
{
IBASE_Release(b1);
b2->Release();
} |
The above test code is assembled into this:
;;;4
void test(IBase* b1, CBase* b2)
;;;5 {
000000
e92d4070
STMFD sp!,{r4-r6,lr}
000004
e1a05000
MOV r5,r0
000008
e1a06001
MOV r6,r1
;;;6 IBASE_Release(b1);
00000c
e5950000
LDR r0,[r5,#0]
000010
e5901004
LDR r1,[r0,#4]
000014
e1a00005
MOV r0,r5
000018
e1a0e00f
MOV lr,pc
00001c
e1a0f001
MOV pc,r1
;;;7 b2->Release();
000020
e1a04006
MOV r4,r6
000024
e5960000
LDR r0,[r6,#0]
000028
e5900004
LDR r0,[r0,#4]
00002c
e5962000
LDR r2,[r6,#0]
000030
e0801002
ADD r1,r0,r2
000034
e1a00006
MOV r0,r6
000038
e1a0e00f
MOV lr,pc
00003c
e1a0f001
MOV pc,r1
;;;9 }
|
We find that IBASE_Release(b1) and b2->Release() generate different
assembly instructions. The v-table of the object generated by ARM C++
does not contain the absolute address of the member function, it
contains an offset value. To get the absolute address of a member
function, we should add this offset value with the base address of the
v-table.
The advantage of this design is that v-tables in an ARM C++ compiled
program can reside in a read only position independent segment (ropi)
which can be loaded to any base addresses and don’t need to made any
adjustment. However, the disadvantage is also obvious, the
runtime speed of a virtual function is slower than other implementation
and the memory layout of a C++ object is not compatible with BREW
Interfaces. That’s why the BREW SDK ONLINE HELP has a note in the
“Extending BREW APIs” section:
The C++ equivalent of the
previous class declaration is shown in
this example:
class IExtensionCls : public IBase
{
int DrawSmiley(IExtensionCls * po, AEEPoint ptCenter, int
nRadius);
int DrawHello(IExtensionCls * po, AEEPoint ptXYOffset);
}
NOTE: This C++ illustration is for explanatory purposes only. You
cannot use C++ yet since
there are issues with the ARM Compiler. We are
looking into these issues and will soon make
this feature available. |
GNU ARM C++ ISSUES
So far, the gnude compiler (http://sourceforge.net/projects/gnude) is
the only ARM C++ compiler, we have tried, that has a compatible object
memory layout with BREW Interfaces. However, there is still some minor
issue:
When linking the program, the linker will report an unresolved link to
__cxa_pure_virtual which is a dummy function that fills the v-table of
an abstract class (class that has at one abstract member function).
This function does nothing and will never be called at runtime. To
solve this, you have to declare this function as an empty function in
you code. If your program uses brewpp.h and brewpp.cpp, you don’t need
to do it because is already done by brewpp.cpp.
CONCLUSIONS
We have proved that we can write BREW applications or extensions using
pure C++ language without lost any speed and memory. We can use BREW
Interfaces as C++ classes and declare C++ classes and register it to
BREW as extensions. We can use all windows desktop C++ compilers (VC++,
BC++, GNU C++) to compile the executable on the emulator. However, due
to the current limitation of the ADV 1.2 compilerwe can only use gnude
ARM compiler to compile our C++ programs. Other ARM compilers are not
tested yet, they might work or not.