TweetFollow Us on Twitter

Ext Code Modules
Volume Number:9
Issue Number:9
Column Tag:Pascal Workshop

External Code Modules
in Pascal

Put code and resources into external modules for expandable applications

By Rob Spencer, East Lyme, Connecticut

Note: Source code files accompanying article are located on MacTech CD-ROM or source code disks.

About the author

Rob Spencer is a biochemist at Pfizer Central Research, and when he’s not helping to develop new pharmaceuticals he’s an avid Mac enthusiast.

Code resources are everywhere. They’re in our operating system as drop-in Extensions. Our applications use them as familiar WDEFs, CDEFs, LDEFs, and MDEFs. We can write our own for extendible applications and development tools like HyperCard, Excel, 4th Dimension, ProGraph, Serius Developer, and LabView. We even see them in screen savers with zillions of drop-in modules. So how do these external chunks of code work, and how can you write your own application to make use of them? Here’s a set of seven successively more complex Pascal experiments that result in a simple structure to support powerful and flexible external code modules.

EXPERIMENT 1: ONE-WAY JUMPS

First comes the most important trick: how to call procedures by address, with parameter passing. I’ve seen this code snippet in many places, including the original HyperCard glue routines (as in Danny Goodman’s HyperCard Developer’s Guide, Bantam Books, 1988, appendix C), MacTutor (Jean de Combret, The Best of MacTutor, vol 5, pp. 246-260), and Symantec magazine (Spring and Autumn 1991 in the THINK Pascal section). All that’s needed is a small inline procedure with a parameter list that exactly matches (in number and type) the procedure we want to remote-call, plus a ProcPtr. The code for Experiment 1 demonstrates; it’s very short:

{1}

program ExperimentOne;

 { ---------------- }

 procedure CallCodeRes (paramA, paramB: integer; var paramC: integer; 
addr: ProcPtr);
 { Note that the param list must exactly  }
 { match that of the called procedure in  }
 { order and type, with the address added }
 { at the end. }
 inline
 $205F, { MOVE.L (A7)+,A0 }
 $4E90; { JSR (A0)        }

 { ---------------- }

 procedure xmMain (x, y: integer; var z: integer);
 { Accept input parameters and do something. }
 begin
 z := x + y;
 end;

 { ============ main ============= }
var
 a, b, c: integer;
begin
 a := 2;
 b := 3;
 c := 0;
 CallCodeRes(a, b, c, @xmMain);
 { Use THINK’s lazy I/O for this demo. }
 ShowText;
 writeLn(‘ a,b,c = ‘, a, b, c);
end.

When this program runs, the correspondence of parameter lists in the CallCodeRes and xmMain procedures makes the compiler set up the stack correctly before it goes to the inline code and makes the jump. We can pass parameters by value or address (var), and in the latter case get information back from the code module (i.e., 2 + 3 = 5). This is all in one little program here, but the descendants of xmMain (the prefix “xm” stands for “external module”) will reside in independently compiled code modules, while y will remain in the host application.

EXPERIMENT 2: ADD A PARAMETER BLOCK

This structure is very useful for debugging code resources inside the THINK environment (such as CDEFs, MDEFs, and WDEFs; see Jean de Combret’s article), but for our purposes it’s not enough. In Experiment 2 (see the Listings section) we pass only one parameter, but it’s a pointer to a parameter block:

{2}

 type
 xmPtr = ^xmBlock;
 xmBlock = record
 request: integer;
 result: integer;
 params: array[1..8] of longint;
 callbackAddr: ProcPtr;
 end;

This block is modeled after HyperCard’s XCMD parameter block. The param longints are used to pass information between the code module and the application, in either direction. These replace the explicit list of parameters in the procedures of Experiment 1. With type coercion these longints can be points, pointers, handles, etc. If you want to pass a string, cast one of these as a StringHandle or StringPtr. The other fields in the parameter block (request, result, callbackAddr) are there to support two-way communication to the module - which is what Experiments 3 and 4 are all about.

EXPERIMENT 3: ADD CALLBACKS

Experiments 1 and 2 satisfy the first reason why you might want to use external code modules: to have a way to send data out to a drop-in module and get answers back. However, external modules can be more useful if they have the ability to manage their own windows, menus, events, and script language commands, as well as specialized data. The difficulty is that there are many things in the application that a separately compiled code module can’t get access to, such as events and globals like window pointers and menu handles. We aren’t going to re-write and re-compile our application every time we write a new module (that’s the whole point), so we need a way for the application to know which modules are “out there” and what messages they know how to receive. All this means that really useful modules need two-way communication to the host application - not just by passing parameters, but by calling each other’s procedures. This is what callbacks allow.

Implementing callbacks takes another copy of the little inline routine to jump back to the application. All we have to do is include the return address (the app’s procedure to which we return) in the parameter block that we pass to the external code. Naturally, that return procedure had better be in a locked segment or we could jump back to the Twilight Zone.

The return address can point to only one procedure in the application, but we’ll want to have many different callback options. The solution is to make that procedure (CallbackDispatcher) a big case statement with the request field of the parameter block as the case selector:

{3}

 procedure CallbackDispatcher (theXmPtr: xmPtr);
 { Don’t let this move or be purged! }
 begin
 case theXmPtr^.request of
 xreqNewXMWindow: 
 begin  { do this... }
 end;
 xreqCloseXMWindow: 
 begin  { do that... }
 end;
 xreqSysBeep: 
 begin  { do this... }
 end;
 otherwise
 { unknown callback request }
 end;
 end;

The values of theXmPtr^.request are constants agreed upon by the application and all external modules. In Experiment 3 these constants are:

{4}

 xreqNewXMWindow = 5001;
 xreqCloseXMWindow = 5002;
 xreqSysBeep = 5003;

The prefix ‘xreq’ stands for ‘external request’; there will be ‘areq’, or ‘application requests’, coming later.

GLUE ROUTINES

Making the use of callbacks convenient for the external module programmer is important. Remember that one reason for using external code is that you (or someone else) may want to add functionality long after the host application is finished and distributed. You shouldn’t have to rediscover how you typecast all the param fields; you just want a simple declaration that you can understand immediately, as if it came out of Inside Macintosh. Every callback has a short glue routine that takes care all this, like this one in Experiment 4:

{5}

 function NewXMWindow (theXmPtr: xmPtr; wBounds: rect; 
 wTitle: str255; wType: integer): WindowPtr;
 begin
 with theXmPtr^ do
 begin
 request := xreqNewXMWindow;
 param[1] := longint(wBounds.topLeft);
 param[2] := longint(wBounds.botRight);
 param[3] := longint(@wTitle);
 param[4] := longint(wType);
 DoCallback(theXmPtr, callbackAddr);
 NewXMWindow := WindowPtr(param[5]);
 if param[5] = 0 then
 result := xmFail
 else
 result := xmSuccess;
 end;
 end;

Within your module code you use NewXMWindow just as you would NewWindow. The glue routine sets up the parameter block, does the callback, and casts the results from the application to the appropriate type. All that matters is that the glue routine and CallbackDispatcher agree exactly on the order and typecasting of the parameters.

Note also how xmPtr^.result is used as an error flag, which most often will be assigned values for success or fail constants. You may also wish to assign it the value of a Toolbox-returned OSErr or ResErr, so that the application could (for example) specifically react to a low memory condition.

EXPERIMENT 4: HOST REQUESTS

We have enough now to implement single-task modules, like most HyperCard XCMD’s. However, a little more discipline and structure can make externals more useful and independent. Since the external can count on callback routines in the application, defined by a set of ‘xreq’ constants and glue routines, it’s only fair that the host application be able to call pre-defined routines in the external, defined by a set of ‘areq’ constants.

Here are the first four defined in Experiment 4:

{6}
 areqOpen = 7000;
 areqClose = 7001;
 areqDoEvent = 7002;
 areqGetInfo = 7003;

These might be part of a “required set” of requests that all externals must accept. You can establish the rule that the application will always call areqOpen when it first calls an external. This gives the external a chance to create its menus, windows, or internal data structures. When it’s closing time, the application must similarly call areqClose, so that the external can release memory, etc. As areqDoEvent suggests, an external should be able to accept normal Mac events and respond appropriately (e.g., by updating its windows). An external must also be able to tell the application something about itself via areqGetInfo, such as its name and some author or copyright information. Experiment 4 provides the structure for these calls but does not implement them all.

The other two request constants in Experiment 4 refer to unique routines that “do the work” :

{7}
 areqDrawMoire = 8001;  
 areqCalcFactorial = 8002;

This is a kludge! It helps to keep Experiment 4 simple, but it requires that the application know these dispatch constants at compile time, which prevents me from adding new, unanticipated externals later - the whole point of using external code. There are at least two solutions to this dilemma: either the application can call the external’s routines by event, such as after a specific menu item or button selection (from menus or buttons derived from the external’s resources), or the application can call the external’s specialized routines by name, perhaps as the result of a script language command. In either case, the application could either “broadcast” the request out to all externals until one accepted it (the way HyperCard passes messages along its hierarchy), or at launch time the application could query all externals and build a small database (of menu item names or script language commands) to help it look up event ownership later.

OVERWRITES AND MEMORY

The HyperCard XCMD parameter block has separate InArgs and OutArgs. I decided not to make this distinction but rather let all 8 parameters be used for data transfer in either direction. However, some discipline is required because the using a callback can overwrite information passed from the application. Consider how you might have your external respond to the areqOpen call at startup time:

As this figure suggests, your DoOpen routine might make three callbacks to set up a window, global variable, and menu, but finally you have to send the xmSuccess message back in response to the original areqOpen request. All three callbacks use the same block (only the app allocates parameter blocks, using NewPtrClear), and so they overwrite the parameters freely. The solution is simply to copy essential information from the parameter block into local variables immediately after information is received, and then put that information back into the block before returning control to the application.

Another lesson from HyperCard: prevent memory headaches by establishing the rule that externals must not dispose of memory that they did not allocate. If you violate this rule, then at least document clearly which callbacks return handles or pointers that the external must dispose itself. In these experiments I obey the rule for windows (the callback CloseXMWindow does nothing more than call DisposeWindow), but I break the rule for menus (the callback GetXMMenu returns a MenuHandle, but the external itself must call DisposeMenu when it closes). You decide where to compromise between clean design and efficiency.

Figure 1.

WHAT’S NEXT?

Experiments 1 to 4 are all small stand-alone programs with listings in this article. Experiments 5, 6, and 7 successively build toward a real event-handling Mac application with drop-in external code modules, but for brevity their code is just on the monthly disk. Here’s what they add.

In Experiment 5 the external code module is finally put into its own unit. The application is filled out to a minimal Mac application with menus and event loop. However, the external module, though separately compiled, must be inserted into the application with ResEdit (just like XCMDs must be inserted into stacks or HyperCard), and the calls to the module are still hard-wired. Though the external code can be separately compiled as a code resource, the listings also show how to include it within the host application during debugging.

In Experiment 6, the module is made responsive to menu and window events. This uses the simple “database” approach, so that when the app receives an event it can find out which external (if any) “owns” the event and should be passed the event for processing. New callbacks are added to support this:

{8}

 function GetXMMenu (theXmPtr: xmPtr; menuName: 
 str15): MenuHandle;
 function CountXMWindows (theXmPtr: xmPtr; 
 justMine: boolean): integer;
 function GetIndXMWindow (theXmPtr: xmPtr;
 i: integer; justMine: boolean; var owner: 
 OSType): WindowPtr;

The module name is shortened to an OSType to simplify and shrink the database.

Finally, in Experiment 7, external modules are truly separated from the application: they become separate files dropped into the application’s folder, which the application finds and loads at launch time. Also, Experiment 7 implements idle calls for externals, so that every external that requests idle time will be called once every pass through the main event loop. Figure 1 shows that the grand finale looks like a typical Mac demo circa 1985, except that nearly all the code and resources are contained in drop-in module files.

CONCLUSION

External modules can be used in many ways. Consider two extremes: you could write a large application for statistics and curve fitting and use very small modules to make it easy to add exotic functions later. Such a module might have no interface at all but just evaluate a mathematical function. Alternatively, like After Dark, the application (or CDEV and INIT) might be rather small and the modules contain most of the code, resources, user interface, and “personality” that the user identifies with the software.

You may want to use external modules to control application bloat, by letting the user drop in just the functionality needed. For in-house development teams, the most Mac-literate programmer might write the application code, then let others write externals, using the callbacks to handle most of the user interface and event details. Similarly, programmers can write externals in any language that can produce pure-code resources, as long as the glue routines are translated.

LISTINGS: EXPERIMENT 2
program ExperimentTwo;
 { Use a parameter block to exchange data }
 { with the called procedure. }
 const
 xmSuccess = 0;
 type
 xmPtr = ^xmBlock;
 xmBlock = record
 request: integer;
 result: integer;
 param: array[1..8] of longint;
 callbackAddr: ProcPtr;
 end;

 { ---------------- }

 procedure CallCodeRes (theXmPtr: xmPtr; addr: ProcPtr);
 inline
 $205F, $4E90;

 { ---------------- }

 procedure xmMain (theXmPtr: xmPtr);
 begin
 if theXmPtr <> nil then
 with theXmPtr^ do
 begin
 param[3] := param[1] + param[2];
 result := xmSuccess;
 end;
 end;

 { ============ main ============= }
 var
 myXmPtr: xmPtr;
begin
 myXmPtr := xmPtr(NewPtrClear(sizeOf(xmBlock)));
 if myXmPtr <> nil then
 with myXmPtr^ do
 begin
 param[1] := 2;
 param[2] := 3;
 param[3] := 0;
 CallCodeRes(myXmPtr, @xmMain);
 ShowText;
 writeLn(‘ answer = ‘, param[3]);
 end;
end.
LISTINGS: EXPERIMENT 3

program ExperimentThree;
 { Add the ability to make ‘callbacks’ to the host appl }

 { ======= common types and constants ======= }
 { These must be the same in the host app and }
 { external modules. }

 type
 xmPtr = ^xmBlock;
 xmBlock = record
 request: integer;
 result: integer;
 param: array[1..8] of longint;
 callbackAddr: ProcPtr;
 end;

 const
 xmSuccess = 0;
 xmFail = -1;
 xreqNewXMWindow = 5001;
 xreqCloseXMWindow = 5002;
 xreqSysBeep = 5003;

 { ==== procedures for the code module ==== }

 { ---- glue routines ---- }

 procedure DoCallback (theXmPtr: xmPtr; addr: ProcPtr);
 inline
 $205F, $4E90;

 { ---------------- }

 function NewXMWindow (theXmPtr: xmPtr; wBounds: rect;         
 wTitle: str255; wType: integer): WindowPtr;
 begin
 with theXmPtr^ do
 begin
 request := xreqNewXMWindow;
 param[1] := longint(wBounds.topLeft);
 param[2] := longint(wBounds.botRight);
 param[3] := longint(@wTitle);
 param[4] := longint(wType);
 DoCallback(theXmPtr, callbackAddr);
 NewXMWindow := WindowPtr(param[5]);
 if param[5] = 0 then
 result := xmFail
 else
 result := xmSuccess;
 end;
 end;

 { ---------------- }

procedure CloseXMWindow(theXmPtr: xmPtr; window: WindowPtr);
 begin
 with theXmPtr^ do
 begin
 request := xreqCloseXMWindow;
 param[1] := longint(window);
 DoCallback(theXmPtr, callbackAddr);
 end;
 end;

 { ---------------- }

 procedure DoBeep (theXmPtr: xmPtr; numBeeps: integer);
 begin
 with theXmPtr^ do
 begin
 request := xreqSysBeep;
 param[1] := longint(numBeeps);
 DoCallback(theXmPtr, callbackAddr);
 end;
 end;

 { --- the external module code that ‘does something’ --- }

 procedure xmMain (theXmPtr: xmPtr);
 { Get a window, draw some circles, }
 { wait a second, and close it.     }
 var
 tempRect: rect;
 tempStr: str255;
 tempLong: longint;
 theWindow: WindowPtr;
 i: integer;
 oldPort: GrafPtr;
 begin
 if theXmPtr <> nil then
 begin
 tempStr := ‘External Module Window’;
 SetRect(tempRect, 100, 100, 300, 300);

 { first callback: get a window from the host app }
 theWindow := NewXMWindow(theXmPtr, tempRect,
 tempStr, noGrowDocProc);
 if theXmPtr^.result = xmSuccess then
 begin
 GetPort(oldPort);
 SetPort(theWindow);
 SetRect(tempRect, 99, 99, 100, 100);
 for i := 1 to 100 do
 begin
 InsetRect(tempRect, -2, -2);
 FrameOval(tempRect);
 end;

 { second callback: make a noise }
 DoBeep(theXmPtr, 3);
 Delay(60, tempLong);

 { third callback: ask the app to close the window }
 CloseXMWindow(theXmPtr, theWindow);
 SetPort(oldPort);
 end;
 end;
 end;

{ === procedures in the host application === }

 procedure CallCodeRes (theXmPtr: xmPtr; addr: ProcPtr);
 inline
 $205F, $4E90;

 { ---------------- }

 procedure CallbackDispatcher (theXmPtr: xmPtr);
 { Don’t let this move or be purged! }
 var
 tempRect: rect;
 tempStr: str255;
 tempInt: integer;
 begin
 if theXmPtr <> nil then
 with theXmPtr^ do
 case request of

 xreqNewXMWindow: 
 begin
 tempRect.topLeft := point(param[1]);
 tempRect.botRight := point(param[2]);
 tempStr := StringPtr(param[3])^;
 tempInt := param[4];
 param[5] := longint(NewWindow(nil, tempRect,
 tempStr, true, tempInt, windowPtr(-1), 
 false, 0));
 end;

 xreqCloseXMWindow: 
 begin
 if param[1] <> 0 then
 DisposeWindow(windowPtr(param[1]));
 end;

 xreqSysBeep: 
 begin
 if not (param[1] in [1..20]) then
 param[1] := 1;
 for tempInt := 1 to param[1] do
 SysBeep(0);
 end;

 otherwise
 { unknown callback request }
 end;
 end;

 { ============ main ============= }

 var
 myXmPtr: xmPtr;
begin
 myXmPtr := xmPtr(NewPtrClear(sizeOf(xmBlock)));
 if myXmPtr <> nil then
 begin
 myXmPtr^.callbackAddr := @CallbackDispatcher;
 CallCodeRes(myXmPtr, @xmMain);
 DisposPtr(ptr(myXmPtr));
 end;
end.
LISTINGS: EXPERIMENT 4
program ExperimentFour;
 { Add ‘areq’ calls from the host app to the module. }
 { Only those procedures that differ from Experiment 3 are }
 { listed here.  The code disk has the complete listing.}
const
 xmSuccess = 0;
 xmFail = -1;
 xreqNewXMWindow = 5001;
 xreqCloseXMWindow = 5002;
 xreqSysBeep = 5003;

 { new in Experiment 4 }
 xmUnknownAppRequest = -2;

 areqOpen = 7000;{ required commands }
 areqClose = 7001;
 areqDoEvent = 7002;
 areqGetInfo = 7003;

 areqDrawMoire = 8001;    { module-specific }
 areqCalcFactorial = 8002;

 procedure xmMain (theXmPtr: xmPtr);
 { Respond to different messages from the host application. }
 { Some messages are required and common to all modules; }
 { some are unique to this module.     }

 { ---- required ----- }

 procedure OpenXModule;
 begin
 { One-time call.  Create any menus }
 { and custom data structures here. }
 end;

 { ----------- }

 procedure CloseXModule;
 begin
 { Release any memory and clean up. }
 end;

 { ----------- }

 procedure DoXmEvent;
 begin
 { Respond to Mac events }
 end;

 { ----------- }

 procedure GetXmInfo;
 { Return name and author info, }
 { like ‘!’ and ‘?’ for XCMDs   }
 var
 nameStr, authorStr: str255;
 begin
 nameStr := ‘Experiment Four Module’;
 authorStr := ‘Rob Spencer  1993’;
 theXmPtr^.param[1] := longint(NewString(nameStr));
 theXmPtr^.param[2] := longint(NewString(authorStr));
 end;

 { ----- unique ---- }

 procedure DrawMoire;
 { This was the entire external in Exp 3 }
 var
 tempRect: rect;
 tempStr: str255;
 tempLong: longint;
 theWindow: WindowPtr;
 i: integer;
 oldPort: GrafPtr;
 begin
 tempStr := ‘External Module Window’;
 SetRect(tempRect, 100, 100, 300, 300);
 theWindow := NewXMWindow(theXmPtr, tempRect, tempStr,
  noGrowDocProc);
 if theXmPtr^.result = xmSuccess then
 begin
 GetPort(oldPort);
 SetPort(theWindow);
 SetRect(tempRect, 99, 99, 100, 100);
 for i := 1 to 100 do
 begin
 InsetRect(tempRect, -2, -2);
 FrameOval(tempRect);
 end;
 DoBeep(theXmPtr, 3);
 Delay(60, tempLong);
 CloseXMWindow(theXmPtr, theWindow);
 SetPort(oldPort);
 end;
 end;

 { ----------- }

 procedure CalculateFactorial;
 { Calculate the factorial of param[1], }
 { put the result in param[2].          }
 var
 n: integer;
 factorial: longint;
 tempStr: str255;
 tempStrHandle: StringHandle;
 begin
 n := theXmPtr^.param[1];
 if (n < 0) or (n > 12) then
 begin
 { Error! return a readable message }
 { so the app may inform the user.  }
 theXmPtr^.result := xmFail;
 tempStr := ‘factorial input out-of-range’;
 tempStrHandle := NewString(tempStr);
 Hlock(handle(tempStrHandle));
 theXmPtr^.param[1] := 
 longint(handle(tempStrHandle));
 end
 else
 begin
 { Ok, do the calculation. }
 factorial := 1;
 while n > 1 do
 begin
 factorial := factorial * n;
 n := n - 1;
 end;
 theXmPtr^.param[2] := factorial;
 end;
 end;

 { ----- xmMain ----- }

 begin
 if theXmPtr <> nil then
 { Dispatch the app’s request }
 begin

 theXmPtr^.result := xmSuccess;
 case theXmPtr^.request of
 areqOpen: 
 OpenXModule;
 areqClose: 
 CloseXModule;
 areqDoEvent: 
 DoXmEvent;
 areqGetInfo: 
 GetXmInfo;
 areqDrawMoire: 
 DrawMoire;
 areqCalcFactorial: 
 CalculateFactorial;
 otherwise
 theXmPtr^.result := xmUnknownAppRequest;
 end;
 end;
 end;

 { ============ main ============= }

 var
 myXmPtr: xmPtr;
 tempStr: str255;
begin
 myXmPtr := xmPtr(NewPtrClear(sizeOf(xmBlock)));
 if myXmPtr <> nil then
 begin
 myXmPtr^.callbackAddr := @CallbackDispatcher;

 { draw a pattern }
 myXmPtr^.request := areqDrawMoire;
 CallCodeRes(myXmPtr, @xmMain);

 { get info about the external code }
 with myXmPtr^ do
 begin
 request := areqGetXmInfo;
 CallCodeRes(myXmPtr, @xmMain);
 ShowText;
 tempStr := StringHandle(param[1])^^;
 DisposHandle(handle(param[1]));
 writeLn(tempStr);
 tempStr := StringHandle(param[2])^^;
 DisposHandle(handle(param[2]));
 writeLn(tempStr);
 end;

 { calculate a factorial & check for errors }
 with myXmPtr^ do
 begin
 { Use Pascal lazy I/O for this demo. }
 ShowText;
 write(‘Enter a number : ‘);
 readLn(param[1]);
 request := areqCalcFactorial;
 CallCodeRes(myXmPtr, @xmMain);
 if result = xmSuccess then
 writeLn(param[1], ‘ ! =’, param[2])
 else
 begin
 tempStr := StringHandle(param[1])^^;
 DisposHandle(handle(param[1]));
 writeLn(tempStr);
 end;
 end;
 end;
 DisposPtr(ptr(myXmPtr));
end.

{ ---- end of listings ---- }

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Adobe Audition CC 2018 11.0.1 - Professi...
Audition CC 2018 is available as part of Adobe Creative Cloud for as little as $19.99/month (or $9.99/month if you're a previous Audition customer). Adobe Audition CC 2018 empowers you to create and... Read more
Adobe After Effects CC 2018 15.0.1 - Cre...
After Effects CC 2018 is available as part of Adobe Creative Cloud for as little as $19.99/month (or $9.99/month if you're a previous After Effects customer). The new, more connected After Effects CC... Read more
Adobe Premiere Pro CC 2018 12.0.1 - Digi...
Premiere Pro CC 2018 is available as part of Adobe Creative Cloud for as little as $19.99/month (or $9.99/month if you're a previous Premiere Pro customer). Adobe Premiere Pro CC 2018 lets you edit... Read more
Adobe Photoshop CC 2018 19.1 - Professio...
Photoshop CC 2018 is available as part of Adobe Creative Cloud for as little as $19.99/month (or $9.99/month if you're a previous Photoshop customer). Adobe Photoshop CC 2018, the industry standard... Read more
Spotify 1.0.69.336. - Stream music, crea...
Spotify is a streaming music service that gives you on-demand access to millions of songs. Whether you like driving rock, silky R&B, or grandiose classical music, Spotify's massive catalogue puts... Read more
rekordbox 5.1.1.0001 - Professional DJ m...
rekordbox is the best way of preparing and managing your tracks, be it at home, in the studio, or even on the plane! It allows you to import music from other music-management software using the... Read more
Mactracker 7.7.1 - Database of all Mac m...
Mactracker provides detailed information on every Mac computer ever made, including items such as processor speed, memory, optical drives, graphic cards, supported OS X versions, and expansion... Read more
Printopia 3.0.6 - Share Mac printers wit...
Run Printopia on your Mac to share its printers to any capable iPhone, iPad, or iPod Touch. Printopia will also add virtual printers, allowing you to save print-outs to your Mac and send to apps.... Read more
Luminar 2018 1.1.0 - Powerful, adaptive,...
Luminar 2018 is the new full-featured image editor that adapts to the way you edit photos. Over 300 essential tools to fix, edit, and enhance your photos with comfort. The future of photo editing is... Read more
Opera 50.0.2762.67 - High-performance We...
Opera is a fast and secure browser trusted by millions of users. With the intuitive interface, Speed Dial and visual bookmarks for organizing favorite sites, news feature with fresh, relevant content... Read more

Latest Forum Discussions

See All

Jydge hints, tips, and tricks - Everythi...
Just released on iOS, Jydge is a prequel to Neon Chrome and is set in the same universe. Not just that, but the games play in pretty similar ways with them both being twin stick shooters full of surprises. As you might expect from a 10tons game,... | Read more »
World of Warships Blitz: A guide to tact...
Ahoy mates! It's time to set out on the high seas for some PvP battles, and ... sorry, actually, World of Warships Blitz has nothing to do with pirates. Let's start over. [Read more] | Read more »
Around the Empire: What have you missed...
Around this time every week we're going to have a look at the comings and goings on the other sites in Steel Media's pocket-gaming empire. We'll round up the very best content you might have missed, so you're always going to be up to date with the... | Read more »
Everything about Hero Academy 2: Part 4...
In this part of our Hero Academy 2 guide, we're going to have a look at some of the tactics you're going to need to learn if you want to rise up the ranks. We're going to start off slow, then get more advanced in the next section. [Read more] | Read more »
All the best games on sale for iPhone an...
Another week has flown by. Sometimes it feels like the only truly unstoppable thing is time. Time will make dust of us all. But before it does, we should probably play as many awesome mobile videogames as we can. Am I right, or am I right? [Read... | Read more »
The 7 best games that came out for iPhon...
Well, it's that time of the week. You know what I mean. You know exactly what I mean. It's the time of the week when we take a look at the best games that have landed on the App Store over the past seven days. And there are some real doozies here... | Read more »
Popular MMO Strategy game Lords Mobile i...
Delve into the crowded halls of the Play Store and you’ll find mobile fantasy strategy MMOs-a-plenty. One that’s kicking off the new year in style however is IGG’s Lords Mobile, which has beaten out the fierce competition to receive Google Play’s... | Read more »
Blocky Racing is a funky and fresh new k...
Blocky Racing has zoomed onto the App Store and Google Play this week, bringing with it plenty of classic kart racing shenanigans that will take you straight back to your childhood. If you’ve found yourself hooked on games like Mario Kart or Crash... | Read more »
Cytus II (Games)
Cytus II 1.0.1 Device: iOS Universal Category: Games Price: $1.99, Version: 1.0.1 (iTunes) Description: "Cytus II" is a music rhythm game created by Rayark Games. It's our fourth rhythm game title, following the footsteps of three... | Read more »
JYDGE (Games)
JYDGE 1.0.0 Device: iOS Universal Category: Games Price: $4.99, Version: 1.0.0 (iTunes) Description: Build your JYDGE. Enter Edenbyrg. Get out alive. JYDGE is a lawful but awful roguehate top-down shooter where you get to build your... | Read more »

Price Scanner via MacPrices.net

Clearance Apple refurbished iMacs available s...
Apple has previous-generation Certified Refurbished 2015 21″ & 27″ iMacs available starting at $849. Apple’s one-year warranty is standard, and shipping is free. The following models are... Read more
How to save $150-$420 on the purchase of a 20...
B&H Photo has 15″ MacBook Pros on sale for up to $200 off MSRP. Shipping is free, and B&H charges sales tax for NY & NJ residents only: – 15″ 2.8GHz Touch Bar MacBook Pro Space Gray (... Read more
How to save $100-$180 on the purchase of a 20...
B&H Photo has 13″ MacBook Airs on sale for $50-$120 off MSRP. Shipping is free, and B&H charges sales tax for NY & NJ residents only: – 13″ 1.8GHz/128GB MacBook Air (MQD32LL/A): $899, $... Read more
Save on Beats: $30-$80 off headphones, earpho...
Walmart has Beats by Dr. Dre on sale on their online store for $30-$80 off MSRP, depending on the item: – Powerbeats3 Wireless Earphones: $134, save $65 – BeatsX Earphones: $109, save $40 – Beats... Read more
Deals on clearance 15″ Apple MacBook Pros wit...
B&H Photo has clearance 2016 15″ MacBook Pros available for up to $800 off original MSRP. Shipping is free, and B&H charges NY & NJ sales tax only: – 15″ 2.7GHz Touch Bar MacBook Pro... Read more
Apple restocked Certified Refurbished 13″ Mac...
Apple has restocked a full line of Certified Refurbished 2017 13″ MacBook Airs starting at $849. An Apple one-year warranty is included with each MacBook, and shipping is free: – 13″ 1.8GHz/8GB/128GB... Read more
How to find the lowest prices on 2017 Apple M...
Apple has Certified Refurbished 13″ and 15″ 2017 MacBook Pros available for $200 to $420 off the cost of new models. Apple’s refurbished prices are the lowest available for each model from any... Read more
The lowest prices anywhere on Apple 12″ MacBo...
Apple has Certified Refurbished 2017 12″ Retina MacBooks available for $200-$240 off the cost of new models. Apple will include a standard one-year warranty with each MacBook, and shipping is free.... Read more
Apple now offering a full line of Certified R...
Apple is now offering Certified Refurbished 2017 10″ and 12″ iPad Pros for $100-$190 off MSRP, depending on the model. An Apple one-year warranty is included with each model, and shipping is free: –... Read more
27″ iMacs on sale for $100-$130 off MSRP, pay...
B&H Photo has 27″ iMacs on sale for $100-$130 off MSRP. Shipping is free, and B&H charges sales tax for NY & NJ residents only: – 27″ 3.8GHz iMac (MNED2LL/A): $2199 $100 off MSRP – 27″ 3.... Read more

Jobs Board

*Apple* Solutions Consultant - Apple (United...
# Apple Solutions Consultant Job Number: 113384559 Brandon, Florida, United States Posted: 10-Jan-2018 Weekly Hours: 40.00 **Job Summary** Are you passionate about Read more
Security Engineering Coordinator, *Apple* R...
# Security Engineering Coordinator, Apple Retail Job Number: 113237456 Santa Clara Valley, California, United States Posted: 18-Jan-2018 Weekly Hours: 40.00 **Job Read more
*Apple* Data Center Site Selection and Strat...
# Apple Data Center Site Selection and Strategy Research Analyst Job Number: 83708609 Santa Clara Valley, California, United States Posted: 18-Jan-2018 Weekly Hours: Read more
Engineering Manager - *Apple* TV - Apple (U...
# Engineering Manager - Apple TV Job Number: 113305053 Santa Clara Valley, California, United States Posted: 05-Dec-2017 Weekly Hours: 40.00 **Job Summary** The Read more
AppleCare Support Engineer for *Apple* Medi...
# AppleCare Support Engineer for Apple Media Products Job Number: 113222855 Santa Clara Valley, California, United States Posted: 14-Nov-2017 Weekly Hours: 40.00 Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.