devxlogo

Tweak Brew’s Component Interfaces to Tackle Any Task

Tweak Brew’s Component Interfaces to Tackle Any Task

ualcomm Brew’s component-oriented architecture and rich collection of components?interfaces, in Brew parlance?makes it easy to find a component to accomplish a particular task, such as decompressing an incoming stream (try IUnzipAStream) or rendering text (an IFont instance). Sometimes, however, you know the job that needs to be done, but there’s just no component that fits?even though there’s a likely-looking interface that would do the job. This begs the question: why re-invent the wheel if you don’t have to? It’s bad enough writing the code, but if you can get away with using an existing Brew interface to encapsulate your functionality, at least you don’t have to write much of the corresponding documentation.

This article will show you how to do just that: start with an existing interface (the IFile interface, in this case) and write an extension that implements that interface while doing something totally different: emulating a simple standard input/output control to facilitate debugging when porting code to Brew.

Introducing the IOManager

Figure 1. IOManager: Here’s the IOManager in action.

The IOManager is a subclass of the IFile interface that permits users to interact with an application using a scrolling text window and input line?much like a simple dumb terminal. Utterly useless for the average commercial application, IOManager is handy if you’re putting together a little test application to prove something, such as when you need to unit test code you’ve ported to Brew or isolate a piece of code to determine the cause of a bug. Figure 1 shows the IOManager in action.

Click here to download the source code accompanying this article.

While the IOManager could have been written as either a completely custom interface or a subclass of the IControl interface, in this article, it’s a subclass of the IFile interface for a couple of reasons. First, its functionality closely resembles that of the old C-style stdin and stdout ports, accessible via file operations in traditional C. This correspondence seemed to carry over nicely in the implementation, and with a little work, ported code can access the IOManager‘s capabilities the same way as other files during testing. Second, for demonstration purposes, the IFile interface is a little more complex (for example, the Readable/Read paradigm requiring asynchronous callbacks), making the IFile interface a better example when showing you how to go about implementing to a Brew interface.

Internally, the IOManager consists of two separate controls, an ITextCtl to manage user input, and an IHTMLViewer, used to show information written to the IOManager or entered in the ITextCtl. Information passed to the IOManager via its Write method inherited from IFile is displayed in the IHTMLViewer, and anything entered in the text control and confirmed using the SELECT key is available to the client application through the IOManager‘s Read method. Structurally, the data for the IOManager implementation looks like this:

struct SIOManager{  //VTBL pointer always comes first.  IOManagerClsVtbl *pvt;  IShell *m_pIShell;  IModule *m_pIModule;  uint16  m_nRefs;  AEECallback  m_cbReadable;  boolean m_bInvokeReadable;  AEECallback m_cbWritable;  AEERect  m_rc;  AEERect  m_rcHtml;  char m_szInput[ BUFFSIZE ], m_szOutput[ BUFFSIZE ],        m_szHTML[ BRSIZE * BUFFSIZE ];  ITextCtl *m_pITextCtl;  IHtmlViewer *m_pIHtml;  IDisplay *m_pIDisplay;};

Most of these fields will be explained in subsequent sections of this article. For now, you should know that:

  • The implementation’s virtual table must be first, because Brew uses C casts to convert between an extension’s interface (its virtual table) and the private data used by the interface (more on this in the next section).
  • The extension uses three buffers: a buffer to contain what the program has written to the extension to display, a buffer to contain what you’ve entered via the text control, and a buffer containing the combined contents of the two formatted as HTML for display to the user using the HTML viewer control (these are the character strings m_szInput, m_szOutput, and m_szHTML, respectively).
  • The m_pITextCtl text control provides all user input handling, and the m_pIHtml HTML viewer control handles displaying both programmatically written and user-entered data (the contents of m_szHTML).

The Interface
Overloading the IFile manager is similar to building out any other Brew extension with one major catch: the virtual table, or, vtable. You must meet, in exactly the same order, the API contract of the IFile object as set out in the vtable. Here’s how ours looks:

struct IOManagerClsVtbl{	INHERIT_IFile(IOManagerCls);	boolean (*fnHandleEvent)(IOManagerCls *pMgr, AEEEvent eCode,                              uint16 wParam, uint32 dwParam);	uint32  (*fnDraw)(IOManagerCls *pMgr);	uint32	(*fnSetRect)(IOManagerCls *pMgr, AEERect *pRect);};

Again, if the beginning of your vtable for your object doesn’t exactly mimic its Brew counterpart you’ll experience some very interesting crashes. Brew helps you out in this endeavor by defining the “INHERIT_IFile” macro. This will declare all the IFile functions in the correct order for you. Not all Brew extensions have an associated inherit macro, but you should take advantage of them whenever you can. Any functions you wish to add other than what’s in the Brew object must be added afterwards. If you get your vtable right, you can now use the new overloaded object anywhere that an IFile object could be used. However, if there are additional functions outside the IFile API, you need to write new macros with which to access them.

// IAStream Methods#define IOMANAGER_AddRef(p) GET_PVTBL(p,IOManager)->AddRef(p)#define IOMANAGER_Release(p) GET_PVTBL(p,IOManager)->Release(p)#define IOMANAGER_Readable(p, pfn,pu) GET_PVTBL(p,IOManager)->Readable(p,pfn,pu)#define IOMANAGER_Read(p, b, c)  GET_PVTBL(p,IOManager)->Read(p, b, c)#define IOMANAGER_Cancel(p,pfn,pu) GET_PVTBL(p,IOManager)->Cancel(p,pfn,pu)// IFile Methods#define IOMANAGER_Write(p, b, c) GET_PVTBL(p,IOManager)->Write(p, b, c)#define IOMANAGER_GetInfo(p, pi) GET_PVTBL(p,IOManager)->GetInfo(p, pi)#define IOMANAGER_Seek(p, o, po) GET_PVTBL(p,IOManager)->Seek(p, o, po)#define IOMANAGER_Truncate(p, t) GET_PVTBL(p,IOManager)->Truncate(p, t)#define IOMANAGER_GetInfoEx(p,pi) GET_PVTBL(p,IOManager)->GetInfoEx(p,pi)#define IOMANAGER_SetCacheSize(p,n) GET_PVTBL(p,IOManager)->SetCacheSize(p,n)#define IOMANAGER_Map(p,ps,dws,pr,fl,dw) GET_PVTBL(p,IOManager)->Map(p,ps,dws,pr,fl,dw)// Control-like methods#define IOMANAGER_HandleEvent(p,e,w,dw) GET_PVTBL(p,IOManager)->HandleEvent(p,e,w,dw)#define IOMANAGER_Redraw(p) GET_PVTBL(p,IOManager)->Redraw(p)#define IOMANAGER_SetActive(p,b) GET_PVTBL(p,IOManager)->SetActive(p,b)#define IOMANAGER_SetRect(p,prc) GET_PVTBL(p,IOManager)->SetRect(p,prc)

Above, you can see the extra macros written to extend the IOManager‘s function set. With these in place, you can now talk to your new object using both your IOManager macros and Brew’s IFile calls. All Brew objects must have two structures: the external vtable and the internal object listed in the previous section. At the beginning of each function, you have to cast the external vtable object to your internal object?as shownbelow:

static uint32 IOManager_AddRef(IOManagerCls *pMgr){	SIOManager *pThis = (SIOManager*)pMgr;	pThis->m_nRefs++;	return pThis->m_nRefs;}

It is through the cast on the first line of the function that you turn a simple list of functions into a structure containing your private data. The vtable is declared in the public header file while the internal structure should be defined in the .c file. Remember to declare all your function implementations ‘static‘ (as has been done above) so they cannot be referenced directly outside your extension. In this way, the vtable becomes the interface to your object. All calls into your static functions must be accessed through the macros that in turn lookup the function address in your vtable.

Construction and Destruction of the Extension
If you’ve ever written a Brew extension before, this source should look very familiar. For those of you who haven’t done it before, here’s a quick tour of the process we went through to design and implement the IOManager. The first step is to obtain a new class ID from Qualcomm’s developer Web site. In the process, you should receive a .bid file. Then add a new extension to the .mif file for your application. When you call ISHELL_CreateInstance with the new class ID, your AEEClsCreateInstance function will be called. Next, add code to that function to check for your new class ID and call your IOManagerCls_New.

Now, take a look at the IOManagerCls_New function, shown in Listing 1.

Your first step is to allocate enough resources for both the vtable and your internal data. With memory in hand, you must link every entry in the vtable with its corresponding internal function. After you’ve got all your function pointers directed in the right places, you have to link your internal object (pThis) with the vtable. This can be done by hand, but it’s much easier to use the “INIT_VTBL” macro provided by Brew. Once that’s established, you need to hold on to references of your shell and module objects and set the module double pointer to your object pointer w. The ISHELL_CreateInstance will pass your object back to the caller and finally initialize your own internal variables.

If all this seems confusing, don’t worry, it is. This is just a brief foray into extension creation. There are other articles that get much more in depth about creating Brew objects. That said, perhaps the easiest way to set up a new object is to copy and paste one from an example and modify the function calls to suit your needs. For an in-depth look at creating new extensions in C and C++, stay tuned to DevX for a future article on the topic.

Tearing down the object is a simple matter. You just release any internal objects and data, free your vtable, and then release the memory you allocated for your extension The code below shows how it’s been done for the IOManager:

uint32 IOManager_Release(IOManagerCls *pMgr){	SIOManager *pThis = (SIOManager*)pMgr;		pThis->m_nRefs--;	if(pThis->m_nRefs != 0) return pThis->m_nRefs;	CALLBACK_Cancel( &pThis->m_cbReadable );	CALLBACK_Cancel( &pThis->m_cbWritable );	FREE_VTBL(pThis->pvt, IOManagerCls);	IHTMLVIEWER_Release( pThis->m_pIHtml );	ITEXTCTL_Release( pThis->m_pITextCtl );	IDISPLAY_Release( pThis->m_pIDisplay );	if ( pThis->m_pIModule ) IMODULE_Release( pThis->m_pIModule );	ISHELL_Release( pThis->m_pIShell );	FREE(pThis);	return 0;}

The code in the release function really speaks for itself. Check to make sure you’re releasing the last reference, cancel any timers you might have set, free your vTable, release your objects, and you’re done.

Handling Incoming Data
Because the IOManager implements IFile, writing text to the display is as simple as calling IOMANAGER_Write:

  nWritten = IOMANAGER_Write(pThis->m_pIIO, sz, STRLEN( sz ));

Under the hood, the method is implemented in IOManager_Write, which must:

  • Determine and create sufficient space in the m_szOutput string for the newly written data, removing old data if needed.
  • Update the HTML stored in m_szHTML with the new data, converting line feeds to the HTML line break directive
    .
  • Redraw the screen with the new content by reloading the HTML viewer with the freshly recreated HTML.

Listing 2 shows IOManager_Write.

The bulk of Listing 2’s code is the skullduggery necessary to handle trimming old content in the event that there’s more data to put in the buffer than the buffer can hold?when the if statement at the beginning of the function holds. If it holds, you search the buffer for the first line feed after the requisite number of characters to be written to the buffer, and use MEMMOVE to slide everything remaining in the buffer to the beginning of the buffer to make room for the newly-written content. The content itself is written to the buffer using STRCAT.

Rather than immediately reprocessing the HTML buffer and redrawing the display, defer these activities until the next pass through the event handler using Brew’s asynchronous resume facility, so that in the event of many small writes to the IOManager you’ll perform this costly operation only once. This is analogous to buffering file system writes and flushing only infrequently, because file system writes are more costly than buffer writes. Do this by scheduling a resumed invocation of the _HandleWrite method, which does the heavy lifting (Listing 3).

This is just more string-munging. Walk the m_szOutput buffer looking for instances of the linefeed character ‘
.’ When you encounter one, write out the corresponding HTML line break
tag to the HTML buffer m_szHTML. For all other characters, simply copy the character straight across. Once this is done, erase the existing contents of the control on the display, and set the control’s data to contain the newly-created HTML.

Handling User Input
User input within the IOManager is a little trickier. Because you don’t want to re-write the various input methods (multitap and potentially T9 or iTap) from scratch, it makes sense to leverage the ITextCtl to do front-line processing of keystrokes on the numeric keypad. This is not without its limitations, however; it essentially makes input line-centric rather than character-centric, but this is a reasonable compromise for these test applications. Consequently, this is the flow of input:

  • When the IOManager is active, the client application passes all events to the IOManager via the IOMANAGER_HandleEvent method, just as if it were a control.
  • Events are shared with the HTML viewer, permitting up/down scrolling of the HTML viewer, and the text control, letting the text control manage all user input.
  • When you press select, the contents of the text control are appended to the end of the input buffer and written to the HTML viewer and the text control emptied.

As the description indicates, this flow of control begins with IOManager_HandleEvent, the implementation of IOMANAGER_HandleEvent:

boolean IOManager_HandleEvent(IOManagerCls *pMgr,                                AEEEvent eCode, uint16 wParam,                                uint32 dwParam){  SIOManager *pThis = (SIOManager*)pMgr;  boolean bResult;  bResult = IHTMLVIEWER_HandleEvent(pThis->m_pIHtml, eCode,                                    wParam, dwParam);  if ( eCode != EVT_KEY ||       ( eCode == EVT_KEY &&          wParam != AVK_UP && wParam != AVK_DOWN ) )  {    bResult = ITEXTCTL_HandleEvent(pThis->m_pITextCtl, eCode,                                     wParam, dwParam);  }  if ( eCode == EVT_KEY && wParam == AVK_SELECT )  {    _HandleRead( pThis );  }  return bResult;}

The _HandleRead function does the necessary buffering of the input into the m_szInput buffer (Listing 4).

The interesting part of the code is at the end, after converting the contents of the text control to a C string, writing the contents to the output buffer, and copying them to the input buffer. That’s where the interface to the client application happens, when the IOManager uses ISHELL_Resume to kick off the client’s Readable callback to indicate that there’s more data available. In turn, the client can call IOMANAGER_Read, which resolves to IOManager_Read:

int32 IOManager_Read(IOManagerCls * pMgr, void * pBuffer, uint32 dwCount){  SIOManager *pThis = (SIOManager*)pMgr;  int nAmount = MIN( dwCount, STRLEN( pThis->m_szInput ) );  if ( !pBuffer || !dwCount ) return 0;    STRNCPY(pBuffer, pThis->m_szInput, nAmount);  // And move the buffer's contents over by that amount.  MEMMOVE(pThis->m_szInput, pThis->m_szInput + nAmount,           BUFFSIZE - nAmount);  return nAmount;}

Naturally, for any of this to happen, the client application must first register a callback using IOMANAGER_Readable, just as if it was using a file or the higher-level abstract IAStream interface:void IOManager_Readable(IOManagerCls * pMgr, PFNNOTIFY pfn, void * pUser){ SIOManager *pThis = (SIOManager*)pMgr; if ( !pfn && !pUser ) { IOManager_Cancel( pMgr, pfn, pUser ); } else { pThis->m_bInvokeReadable = TRUE; CALLBACK_Init( &pThis->m_cbReadable, pfn, pUser ); if ( STRLEN( pThis->m_szInput ) ) { ISHELL_Resume( pThis->m_pIShell, &pThis->m_cbReadable ); pThis->m_bInvokeReadable = FALSE; } }}Readable first stores the newly-provided callback information (canceling the existing callback if the provided callback is NULL) and then actually invoking the callback if there’s any data to read. Otherwise, the code you saw in _HandleRead will invoke the callback once more data has been read. The m_bInvokeReadable flag ensures that you call a given readable callback only once; per the Brew documentation, Readable must be called each time you want to know when there’s more data available to read.

Between the implementation of Readable and Read, this allows your interface’s clients to treat the interface just as if it were any other IAStream implementation.

Component Riffing
While Brew doesn’t provide a framework for customizing the behavior of individual components, it does let you easily implement new components to existing interfaces using the INHERIT_ macros provided. Using an existing interface and providing your own functionality is a good approach when you recognize the fit between your component and an existing component’s interface, and you can capitalize on re-using the documentation and knowledge about a specific interface, helping speed your overall development.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist