Login | Register
My pages Projects Community openCollabNet

Developing BREW Application Using Pure C++

 
Darklen
caesarshen@gmail.com
9.8.2005
  1. Developing BREW Application Using Pure C++
    1. GOAL
    2. PROBLEMS
      1. Access member functions
      2. Cast between BREW Interfaces
      3. Declare BREW Extensions
      4. Instantiate BREW Extensions
      5. BREW Callbacks
    3. THE MEMORY LAYOUT OF BREW INTERFACES
    4. C++ EQUIVALENCE OF BREW INTERFACES
    5. DIFFERENT BETWEEN C FUNCTIONS AND C++ MEMBER FUNCTIONS
    6. SOLUTIONS
    7. VISUAL C++ ISSUES
      1. ADV1.2 ARM C++ COMPILER ISSUES
    8. GNU ARM C++ ISSUES
    9. 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:

  1. Allocate the memory for both the object and v-table.
  2. Fill the v-table with function pointers.
  3. Set the value of pvt to the address of the v-table.
  4. 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.

  BREW Interface Memory Layout

 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.