TweetFollow Us on Twitter

Human Resources

Volume Number: 18 (2002)
Issue Number: 10
Column Tag: QuickTime Toolkit

Human Resources

Adding Macintosh Resources to a Windows QuickTime Application

by Tim Monroe

Introduction

Macintosh applications use resources in a wide variety of ways. A few of these are: to specify their menus and menu items, to define the appearance of windows and dialog boxes displayed by the applications, to hold strings and other localizable data, and to store fonts, sounds, pictures, custom cursors, and icons. These resources -- let's call them Macintosh resources -- are stored in the application's resource fork, which is automatically opened when the application is launched. The application can retrieve a particular resource by calling Resource Manager functions like GetResource and GetIcon.

Windows applications also use things called resources for many of the same purposes, but these resources are not the same as Macintosh resources. These resources -- let's call them Windows resources -- are stored inside the executable file (the .exe file) and can be loaded programmatically using functions like LoadResource and LoadIcon. Typically, an application's Windows resources are defined using a resource script (a file whose name ends in ".rc") and are automatically inserted into the executable file when the application is built.

As we've seen in many previous articles, it's often useful to use Macintosh resources in our QuickTime applications, whether those applications run on Macintosh or Windows computers. For instance, when we once wanted to elicit a URL from the user, we called GetNewDialog to display a dialog box defined by the application's resources; then we called ModalDialog to handle user actions in the dialog box. Figure 1 shows the dialog box on Macintosh systems, and Figure 2 shows the dialog box on Windows systems.


Figure 1: The Open URL dialog box (Macintosh)


Figure 2: The Open URL dialog box (Windows)

Let's consider another example. Recently I added a properties panel to QuickTime Player, to display information about movie tracks. I constructed the panel on a Macintosh computer, using a text description that is converted by the tool Rez into a Macintosh resource. Figure 3 shows the panel on the Mac OS 9, and Figure 4 shows the panel on Windows. (This panel is not included with any shipping version of QuickTime Player, so don't bother looking for it.)


Figure 3: The Movie Track Properties panel (Mac OS 9)


Figure 4: The Movie Track Properties panel (Windows)

The main advantage to using Macintosh resources for dialog boxes on both operating systems is that we can use the exact same resource description on both systems, and we can use the exact same code to display and manage the dialog boxes. On Windows, this magic is provided by the QuickTime Media Layer (or QTML), which we considered in an earlier article ("2001: A Space Odyssey" in MacTech, January 2001). In this article, I want to focus on how to attach Macintosh resources to a Windows application. We got a preliminary taste of doing this in that earlier article, where we saw how to use the tools Rez and RezWack on Windows to convert a resource description into a resource file and attach it to an application. Here, I want to investigate this process in a bit greater detail and to show how to configure our Windows development environment, Microsoft Visual Studio, to perform these steps automatically each time we build our application.

Some programmers, of course, prefer to do most of their software development on Macintosh computers, so it's useful to see how to perform these steps on a Mac. Metrowerks' CodeWarrior integrated development environment (IDE) for Macintosh supplies compilers and linkers for Windows targets; moreover, the QuickTime software development kit (SDK) provides CodeWarrior projects configured to build Windows applications for virtually all of the sample code applications. All that's missing is a Macintosh version of the RezWack tool. In this article, we'll see how to fill in that gap. First we'll develop a standalone application that does this, and then we'll see how to construct a plug-in for the CodeWarrior IDE that performs the RezWack step as part of the build process. By the end of this article, you'll know how to create complete Windows applications that contain Macintosh resources, whether you prefer to program on Macintosh or Windows systems.

Before we begin, I should mention that having a single resource description for all of our target platforms is a wonderful goal that is not always achievable in practice, at least if we want to pay attention to Apple's human interface guidelines. The reason for this is simply that the Aqua appearance on Mac OS X has different layout requirements than the "classic" Mac OS 8 and 9 appearance. For instance, button labels are usually larger under Aqua than under earlier systems, so we need to make our buttons larger. Figure 5 shows the movie track properties panel when displayed on a computer running Mac OS X. Here the entire dialog box is bigger, to be able to contain the larger controls and other dialog items.


Figure 5: The Movie Track Properties panel (Mac OS X)

Development on Windows

Creating Resource Files

Suppose, then, that we're writing a QuickTime application that is to run on Windows and we want to do our development on Windows using the Visual Studio environment. To make things as easy as possible, we'll construct our Macintosh resources by creating a text file that contains a resource description. For instance, Listing 1 shows the resource description of the 'DITL' resource for the dialog box shown in Figures 3 and 4.

Listing 1: Resource description for the Movie Track Properties panel

MIAMProperties.r
resource 'DITL' (kResourceID) {
   {   /* array DITLarray: 12 elements */
      /* [1] */
      {8, 10, 25, 132},
      StaticText {
         disabled,
         "Background Color:"
      },
      /* [2] */
      {29, 12, 51, 138},
      UserItem {
         enabled
      },
      /* [3] */
      {30, 146, 50, 207},
      Button {
         enabled,
         "Set..."
      },
      /* [4] */
      {16, 7, 60, 214},
      UserItem {
         enabled
      },
      /* [5] */
      {74, 7, 90, 214},
      CheckBox {
         enabled,
         "Auto-Play Child Movie"
      },
      /* [6] */
      {94, 7, 110, 214},
      CheckBox {
         enabled,
         "Frame Step in Child Movie"
      },
      /* [7] */
      {120, 10, 136, 132},
      StaticText {
         disabled,
         "Data Reference Type: "
      },
      /* [8] */
      {120, 132, 136, 207},
      StaticText {
         disabled,
         "URL"
      },
      /* [9] */
      {140, 12, 156, 132},
      StaticText {
         enabled,
         ""
      },
      /* [10] */
      {138, 146, 158, 207},
      Button {
         enabled,
         "Set..."
      },
      /* [11] */
      {67, 10, 68, 215},
      UserItem {
         disabled
      },
      /* [12] */
      {115, 10, 116, 215},
      UserItem {
         disabled
      }
   }
};

The QuickTime SDK for Windows provides the tool Rez, which converts a resource description into a resource file. We can execute this line of code in a DOS console window to create a resource file:

QuickTimeSDK\QTDevWin\Tools\Rez 
               -i "QuickTimeSDK\QTDevWin\RIncludes" -i . 
               MIAMProperties.r -o MIAMProperties.qtr

Here, Rez converts the resource descriptions in the file MIAMProperties.r into resources in the resource file MIAMProperties.qtr. Rez looks in the current directory (.) and in the directory QuickTimeSDK\QTDevWin\RIncludes for any included .r files. (Of course, your paths may differ, depending on where you install the QuickTime SDK tools and .r files.)

The suffix ".qtr" is the preferred filename extension on Windows for files that contain Macintosh resources. When we are building an application, say QTWiredActions.exe, we should therefore name the resource file "QTWiredActions.qtr". In our application code, we need to explicitly open that resource file; to do this, we can use the code Listing 2.

Listing 2: Opening an application's resource file

WinMain
myLength = GetModuleFileName(NULL, myFileName, MAX_PATH);
if (myLength != 0) {
   NativePathNameToFSSpec(myFileName, &gAppFSSpec, 
                                                            kFullNativePath);
   gAppResFile = FSpOpenResFile(&gAppFSSpec, fsRdWrPerm);
   if (gAppResFile != kInvalidFileRefNum)
      UseResFile(gAppResFile);
}

Note that we do not need this code in our Macintosh applications; as we noted earlier, on the Mac an application's resource fork is automatically opened when the application is launched.

Embedding Resource Files in an Application

When developing and testing an application, it's okay to have to work with two files (the application file and the Macintosh resource file). But when we want to distribute an application, it's desirable to combine these two files into a single executable file. This is the job that RezWack is designed to accomplish. RezWack creates a new file that appends the resource data to the data in the executable file (and also appends some additional data to alert QTML to the fact that the .exe file contains some Macintosh resource data). We can call RezWack like this:

QuickTimeSDK\QTDevWin\Tools\RezWack -d QTWiredActions.exe 
               -r QTWiredActions.qtr -o TempName.exe
del QTWiredActions.exe
ren TempName.exe QTWiredActions.ex

RezWack does not allow us to overwrite either of the two input files, so we need to save the executable file under a temporary name, delete the original .exe file, and then rename the output file to the desired name. Once we've executed these commands, the file QTWiredActions.exe contains the original executable code and the Macintosh resources that were contained in the file QTWiredActions.qtr.

Even if we have inserted the resource data into the executable file using RezWack, we still need to explicitly open the resource file at runtime; so our applications should always include the code in Listing 2, whether the resource data is in a separate file or is included in the executable file.

Adding a Post-Link Step

It would get tedious really fast to have to type the Rez and RezWack commands shown above in a DOS console window every time we rebuild our application. We can simplify our work by creating a batch file that contains these commands and by executing the batch file automatically as a custom post-link step. Listing 3 shows the batch file that we use when building the debug version of the application QTWiredActions.exe.

Listing 3: Rezzing and RezWacking an application's resources

MyRezWackDebug.bat
REM *** batch program to embed our Macintosh
REM *** resources into our application file
..\..\QTDevWin\Tools\Rez -i "..\..\QTDevWin\RIncludes" -i . 
            QTWiredActions.r -o QTWiredActions.qtr
..\..\QTDevWin\Tools\RezWack -d .\Debug\QTWiredActions.exe 
            -r QTWiredActions.qtr -o .\Debug\TEMP.exe -f
del .\Debug\QTWiredActions.exe
REM *** now rename new file to previous name
ren .\Debug\TEMP.exe QTWiredActions.exe

You'll notice that the paths to the Rez and RezWack tools are relative to the location of the batch file, which we keep in the same folder as the Visual Studio project file; you may need to edit this file to set the correct paths for your particular folder arrangement.

We can tell Visual Studio to execute this file as a custom post-link step by adjusting the project settings. Figure 6 shows the appropriate settings panel.


Figure 6: Setting a custom post-link step

Development on Macintosh

So what does RezWack actually do? We already know that it adds Macintosh resource data to a Windows executable file. But we need to uncover a bit more detail here if we are to be able to write a tool that performs resource wacking on Macintosh computers. Here's the essential information: RezWack creates a new file that begins with the data in the Windows executable file, followed immediately by the Macintosh resource data, padded to the nearest 4K boundary; this is all followed by some RezWack tag data. Figure 7 illustrates the structure of a file created by RezWack; let's call this a wacked file.


Figure 7: The structure of a wacked file

The RezWack tag data is a 36-byte block of data that describes the layout of the wacked file. It specifies the offset of the executable data from the beginning of the file (which is usually 0) and the size of the executable data; it also specifies the offset of the resource data from the beginning of the file and the size of the resource data. The tag data ends with this 12-byte identifier: "mkQuickTime(TM)". (What's the "mk"? I strongly suspect that they are the initials of the engineer who implemented the RezWack tool.)

The idea here is that QuickTime can look at the last 12 bytes of a Windows executable file to determine whether it contains any Macintosh resources. If it does, then QuickTime can look at the last 36 bytes of the file to get the offset into the file of those resources and the size of the resource data.

Let's define a couple of constants that we can use to help us in the wacking process:

#define kRezWackTag                     "mkQuickTime(TM)"
#define kRezWackTagSize                  12   

Then we can define this data structure for the RezWack tag data:

typedef struct RezWackTagData {
   UInt32         fDataTag;                  // must be 'data'
   UInt32         fDataOffset;               // offset of binary data
   UInt32         fDataSize;                  // size of binary data
   UInt32         fRsrcTag;                  // must be 'rsrc'
   UInt32         fRsrcOffset;               // offset of resource data
   UInt32         fRsrcSize;                  // size of resource data
   char            fWackTag[kRezWackTagSize];
} RezWackTagData, *RezWackTagDataPtr;

We'll use this later, when we finally get around to building wacked files.

Setting up a Droplet

Our current goal is to develop a standalone application -- let's call it "RezWack PPC" -- that we can use as a droplet. That is, we'll compile our Windows code using CodeWarrior on the Macintosh and then drag the executable file created by CodeWarrior onto our droplet, which finds the appropriate resource file and creates a new file containing the executable data, the resource data, and the RezWack tag data.

We want the final wacked file to have the standard ".exe" suffix, so we need to tell CodeWarrior to generate an intermediate executable file with some other suffix. (Remember, RezWack won't overwrite either of its input files and so neither should we.) Let's use the suffix ".bin", as shown in Figure 8.


Figure 8: Setting the intermediate file name

Our droplet should accept these binary files, look for a resource file with the suffix ".qtr", and then create the final wacked file with the ".exe" suffix. To make RezWack PPC act like a droplet, we need to modify the function QTApp_HandleOpenDocumentAppleEvent. Listing 4 shows our new definition of this function.

Listing 4: Handling files dropped onto RezWack PPC

QTApp_HandleOpenDocumentAppleEvent
PASCAL_RTN OSErr QTApp_HandleOpenDocumentAppleEvent 
         (const AppleEvent *theMessage, AppleEvent *theReply, 
         long theRefcon)
{
#pragma unused(theReply, theRefcon)
   long               myIndex;
   long               myItemsInList;
   AEKeyword      myKeyWd;
   AEDescList      myDocList;
   long               myActualSize;
   DescType         myTypeCode;
   FSSpec            myFSSpec;
   OSErr            myIgnoreErr = noErr;
   OSErr            myErr = noErr;
   // get the direct parameter and put it into myDocList
   myDocList.dataHandle = NULL;
   myErr = AEGetParamDesc(theMessage, keyDirectObject, 
            typeAEList, &myDocList);
   // count the descriptor records in the list
   if (myErr == noErr)
      myErr = AECountItems(&myDocList, &myItemsInList);
   else
      myItemsInList = 0;
   // open each specified file
   for (myIndex = 1; myIndex <= myItemsInList; myIndex++)
      if (myErr == noErr) {
         myErr = AEGetNthPtr(&myDocList, myIndex, typeFSS, 
            &myKeyWd, &myTypeCode, (Ptr)&myFSSpec, 
            sizeof(myFSSpec), &myActualSize);
         if (myErr == noErr) {
            FInfo      myFinderInfo;
            // verify that the file type is kWinBinaryFileType or kWinLibraryFileType
            myErr = FSpGetFInfo(&myFSSpec, &myFinderInfo);
            if (myErr == noErr)
               if ((myFinderInfo.fdType == kWinBinaryFileType) 
            || (myFinderInfo.fdType == kWinLibraryFileType))
                  QTRW_CreateRezWackedFileFromBinary(&myFSSpec);
         }
      }
   if (myDocList.dataHandle)
      myIgnoreErr = AEDisposeDesc(&myDocList);
   QTFrame_QuitFramework();      // act like a droplet and close automatically
   return(myErr);
}

As you can see, we look for files of type kWinBinaryFileType and kWinLibraryFileType, which have these file types:

#define kWinBinaryFileType            FOUR_CHAR_CODE('DEXE')
#define kWinLibraryFileType            FOUR_CHAR_CODE('iDLL')

When we find one of these types of files, we call QTRW_CreateRezWackedFileFromBinary to do the necessary resource wacking. Notice that we call QTFrame_QuitFramework to quit the application once we are done processing the files dropped on it.

Wacking the Resources

Listing 5 shows our definition of QTRW_CreateRezWackedFileFromBinary; this function simply configures two additional file system specification records (one for the Macintosh resource file and one for the final wacked file) and then calls another function, QTRW_RezWackWinBinaryAndMacResFile, to do the real work. As indicated above, we assume that the resource file has the same name as the binary file but with the ".qtr" suffix and that the final wacked file has the same name as the binary file but with the ".exe" suffix. (I'll leave it as an exercise for the reader to implement less restrictive naming for the three files we need to work with.)

Listing 5: Setting up names for the resource and wacked files

QTRW_CreateRezWackedFileFromBinary
OSErr QTRW_CreateRezWackedFileFromBinary 
            (FSSpecPtr theBinFSSpecPtr)
{
   FSSpec         myResSpec;
   FSSpec         myExeSpec;
   OSErr         myErr = paramErr;
   if (theBinFSSpecPtr == NULL)
      goto bail;
   // currently, we suppose that the Macintosh resource file has the same name as the 
   // binary, but with "qtr" file name extension
   myResSpec = *theBinFSSpecPtr;
   myResSpec.name[myResSpec.name[0] - 2] = 'q';
   myResSpec.name[myResSpec.name[0] - 1] = 't';
   myResSpec.name[myResSpec.name[0] - 0] = 'r';
   // currently, we suppose that the Windows executable file has the same name as the 
   // binary, but with "exe" file name extension
   myExeSpec = *theBinFSSpecPtr;
   
   myExeSpec.name[myExeSpec.name[0] - 2] = 'e';
   myExeSpec.name[myExeSpec.name[0] - 1] = 'x';
   myExeSpec.name[myExeSpec.name[0] - 0] = 'e';
   myErr = QTRW_RezWackWinBinaryAndMacResFile(
            theBinFSSpecPtr, &myResSpec, &myExeSpec);
bail:
   return(myErr);
}
QTRW_RezWackWinBinaryAndMacResFile is the key function in RezWack PPC. It takes the binary file 
created by CodeWarrior and the Macintosh resource file and then creates the desired wacked file. 
First, it creates an empty file, having the same type and creator as the original binary file:

FSpGetFInfo(theBinFSSpecPtr, &myFileInfo);
FSpCreate(theExeFSSpecPtr, myFileInfo.fdCreator, 
            myFileInfo.fdType, 0);

Then QTRW_RezWackWinBinaryAndMacResFile opens all three relevant files, using FSpOpenDF to open the binary and wacked files, and FSpOpenRF to open the resource file. Actually, we want our droplet to be a bit more flexible about handling resource files; it's possible for resource data to be stored in a data fork of a file, particularly if the resource data was created on a Windows computer (perhaps using the Rez tool). So first we'll attempt to open a resource fork having the appropriate name; if we can't find it or if its length is 0, we'll attempt to open a data fork having the appropriate name. Listing 6 shows the code that accomplishes this.

Listing 6: Opening the resource file

QTRW_RezWackWinBinaryAndMacResFile
myErr = FSpOpenRF(theResFSSpecPtr, fsRdPerm, &myResFile);
if (myErr != noErr) {
   myTryDataFork = true;
} else {
   // it's possible that the resource fork exists but is 0-length; if so, try the data fork
   GetEOF(myResFile, &mySizeOfRes);
   if (mySizeOfRes == 0)
      myTryDataFork = true;
}
if (myTryDataFork) {
   // close the open resource file (presumably a 0-length resource fork)
   if (myResFile != -1)
      FSClose(myResFile);
   myErr = FSpOpenDF(theResFSSpecPtr, fsRdPerm, &myResFile);
   if (myErr != noErr)
      goto bail;
}

Once we've got all three files open, it's really quite simple to construct the wacked file. We copy the executable data from the binary file into the output file and copy the resource data from the resource file into the output file. Then we pad the file data to the nearest 4K boundary, as shown in Listing 7.

Listing 7: Padding the executable and resource data

QTRW_RezWackWinBinaryAndMacResFile
mySizeOfExe = mySizeOfBin + mySizeOfRes;
while ((mySizeOfExe + sizeof(myTagData)) % (4 * 1024) != 0) {
   char      myChar = '\0';
   mySize = 1;
   myErr = FSWrite(myExeFile, &mySize, &myChar);
   if (myErr != noErr)
      goto bail;
   mySizeOfExe++;
}

The last thing we need to do is append the RezWack tag data. Listing 8 shows the code we use to do this. Notice that the tag data must be in big-endian byte order (as is typical for QuickTime-related data).

Listing 8: Appending the RezWack tag data

QTRW_RezWackWinBinaryAndMacResFile
myTagData.fDataTag = EndianU32_NtoB((long)'data');
myTagData.fDataOffset = 0L;
myTagData.fDataSize = EndianU32_NtoB(mySizeOfBin);
myTagData.fRsrcTag = EndianU32_NtoB((long)'rsrc');
myTagData.fRsrcOffset = EndianU32_NtoB(mySizeOfBin);
myTagData.fRsrcSize = EndianU32_NtoB(mySizeOfRes);
strncpy(myTagData.fWackTag, kRezWackTag, kRezWackTagSize);
mySize = sizeof(myTagData);
myErr = FSWrite(myExeFile, &mySize, &myTagData);

And we're done! Listing 9 shows the complete definition of QTRW_RezWackWinBinaryAndMacResFile.

Listing 9: Creating a wacked file

QTRW_RezWackWinBinaryAndMacResFile
OSErr QTRW_RezWackWinBinaryAndMacResFile 
      (FSSpecPtr theBinFSSpecPtr, FSSpecPtr theResFSSpecPtr, 
         FSSpecPtr theExeFSSpecPtr)
{
   FInfo                  myFileInfo;
   short                  myBinFile = -1;
   short                  myResFile = -1;
   short                  myExeFile = -1;
   long                     mySizeOfBin = 0L;
   long                     mySizeOfRes = 0L;
   long                     mySizeOfExe = 0L;
   long                     mySize = 0L;
   long                     myData = 0L;
   Handle                  myHandle = NULL;
   RezWackTagData      myTagData;
   Boolean               myTryDataFork = false;
   OSErr                  myErr = paramErr;
   // make sure we are passed three non-NULL FSSpecPtr's
   if ((theBinFSSpecPtr == NULL) 
            || (theResFSSpecPtr == NULL) 
            || (theExeFSSpecPtr == NULL))
      goto bail;
   // get the creator and file type of the binary file
   myErr = FSpGetFInfo(theBinFSSpecPtr, &myFileInfo);
   if (myErr != noErr)
      goto bail;
   // create the final executable file
   myErr = FSpCreate(theExeFSSpecPtr, myFileInfo.fdCreator, 
            myFileInfo.fdType, 0);
   if ((myErr != noErr) && (myErr != dupFNErr))
      goto bail;
   myErr = FSpOpenDF(theExeFSSpecPtr, fsRdWrPerm, 
            &myExeFile);
   if (myErr != noErr)
      goto bail;
   
   // open the resource file; it's possible that the resources are stored in the data fork
   // (particularly if the resource file was built on Windows); so make sure we've got a 
   // non-0-length resource file
   myErr = FSpOpenRF(theResFSSpecPtr, fsRdPerm, &myResFile);
   if (myErr != noErr) {
      myTryDataFork = true;
   } else {
      // it's possible that the resource fork exists but is 0-length; if so, try the data fork
      GetEOF(myResFile, &mySizeOfRes);
      if (mySizeOfRes == 0)
         myTryDataFork = true;
   }
   if (myTryDataFork) {
      // close the open resource file (presumably a 0-length resource fork)
      if (myResFile != -1)
         FSClose(myResFile);
      myErr = FSpOpenDF(theResFSSpecPtr, fsRdPerm, 
            &myResFile);
      if (myErr != noErr)
         goto bail;
   }
   myErr = FSpOpenDF(theBinFSSpecPtr, fsRdPerm, &myBinFile);
   if (myErr != noErr)
      goto bail;
   // copy the binary data into the final executable file
   myErr = SetEOF(myExeFile, 0);
   if (myErr != noErr)
      goto bail;
   myErr = GetEOF(myBinFile, &mySizeOfBin);
   if (myErr != noErr)
      goto bail;
   myHandle = NewHandleClear(mySizeOfBin);
   if (myHandle == NULL) {
      myErr = MemError();
      goto bail;
   }
   myErr = SetFPos(myBinFile, fsFromStart, 0);
   if (myErr != noErr)
      goto bail;
   myErr = FSRead(myBinFile, &mySizeOfBin, *myHandle);
   if (myErr != noErr)
      goto bail;
   myErr = SetFPos(myExeFile, fsFromStart, 0);
   if (myErr != noErr)
      goto bail;
   myErr = FSWrite(myExeFile, &mySizeOfBin, *myHandle);
   if (myErr != noErr)
      goto bail;
   FSClose(myBinFile);   
   DisposeHandle(myHandle);
   // copy the resource data into the final executable file
   myErr = GetEOF(myResFile, &mySizeOfRes);
   if (myErr != noErr)
      goto bail;
   myHandle = NewHandleClear(mySizeOfRes);
   if (myHandle == NULL) {
      myErr = MemError();
      goto bail;
   }
   myErr = SetFPos(myResFile, fsFromStart, 0);
   if (myErr != noErr)
      goto bail;
   myErr = FSRead(myResFile, &mySizeOfRes, *myHandle);
   if (myErr != noErr)
      goto bail;
   
   myErr = FSWrite(myExeFile, &mySizeOfRes, *myHandle);
   if (myErr != noErr)
      goto bail;
   FSClose(myResFile);   
   DisposeHandle(myHandle);
   // pad the final executable file so that it ends on a 4K boundary
   mySizeOfExe = mySizeOfBin + mySizeOfRes;
   while ((mySizeOfExe + sizeof(myTagData)) % (4 * 1024) 
            != 0) {
      char      myChar = '\0';
      mySize = 1;
      myErr = FSWrite(myExeFile, &mySize, &myChar);
      if (myErr != noErr)
         goto bail;
      mySizeOfExe++;
   }
   // add on the special RezWack tag data
   myTagData.fDataTag = EndianU32_NtoB((long)'data');
   myTagData.fDataOffset = 0L;
   myTagData.fDataSize = EndianU32_NtoB(mySizeOfBin);
   myTagData.fRsrcTag = EndianU32_NtoB((long)'rsrc');
   myTagData.fRsrcOffset = EndianU32_NtoB(mySizeOfBin);
   myTagData.fRsrcSize = EndianU32_NtoB(mySizeOfRes);
   strncpy(myTagData.fWackTag, kRezWackTag, 
            kRezWackTagSize);
   mySize = sizeof(myTagData);
   myErr = FSWrite(myExeFile, &mySize, &myTagData);
   FSClose(myExeFile);
bail:
   return(myErr);
}

CodeWarrior Plug-Ins

Our RezWack PPC droplet is a great little tool, but things would be even more convenient if we could get CodeWarrior to perform the resource wacking automatically -- in the same way that we earlier added a post-link step to our Microsoft Visual Studio projects to run the Rez and RezWack tools automatically each time we build an application that uses Macintosh resources. Indeed this is possible, but it's significantly more complicated than just writing a batch file containing a few commands that are executed at the proper time. We need to write a CodeWarrior plug-in, a code module that extends the capabilities of the CodeWarrior IDE, to do this post-link step for us. We also will want to write a CodeWarrior settings panel plug-in, to allow us to specify the names of the resource file and the final output file. Figure 9 shows our settings panel.


Figure 9: The RezWack settings panel

In this section, we'll take a look at the major steps involved in writing a CodeWarrior post-linker plug-in and a settings panel plug-in for wacking Macintosh resources into Windows applications. We won't have space to step through the entire process, but I'll try to touch on the most important points. Thankfully, most of the hard engineering is already done, for two reasons. First, we'll be able to use the QTRW_RezWackWinBinaryAndMacResFile function defined above (Listing 9) virtually unchanged within our post-linker plug-in code. And second, Metrowerks provides an SDK for writing CodeWarrior plug-ins that includes extensive documentation and several sample plug-in projects. Most of our work will consist of adapting two of the existing samples to create a RezWack post-linker and setting panel.

Writing a Post-Linker Plug-In

Let's begin by writing a post-linker plug-in. The first thing to do is download the CodeWarrior SDK from the Metrowerks web site. (See the end of this article for the exact location.) I installed the SDK directly into the "Metrowerks CodeWarrior" folder on my local machine. Then I duplicated the sample project called "Sample_Linker". (There is no sample post-linker project, but it will be easy enough to convert their linker into a post-linker.) Let's call our new project "CWRezWack_Linker". Once we rename all of the source code files and project files appropriately, we'll have the project shown in Figure 10. (The file CWRezWack.rsrc is new and contains a number of string resources that we'll use to report problems that occur during the post-linking; see the section "Handling Post-Link Errors" below.)


Figure 10: The RezWack post-linker project window

The sample linker project provided by Metrowerks includes both Macintosh and Windows targets, but I have removed the Windows target for simplicity; after all, we don't need a RezWack post-linker on Windows. As you can see, our project contains two source code files, CWRezWack.c and CWRezWackUtils.cpp. These are just renamed versions of the original Sample_Linker.c and SampleUtils.cpp. We won't modify CWRezWackUtils.cpp further, except to update the included header file from SampleUtils.h to CWRezWackUtils.h.

In the file CWRezWack.c, we want to remove any code that supports disassembling. We'll remove the entire definition of the Disassemble function as well as its function declaration near the top of the file. Also, we'll rework the function CWPlugin_GetDropInFlags so that it looks like Listing 10.

Listing 10: Specifying the plug-in capabilities

CWPlugin_GetDropInFlags
CWPLUGIN_ENTRY(CWPlugin_GetDropInFlags)
            (const DropInFlags** flags, long* flagsSize)
{
   static const DropInFlags sFlags = {
      kCurrentDropInFlagsVersion,
      CWDROPINLINKERTYPE,
      DROPINCOMPILERLINKERAPIVERSION_7,
      isPostLinker | cantDisassemble,         /* we are a post-linker */
      0,
      DROPINCOMPILERLINKERAPIVERSION
   };
   *flags = &sFlags;
   *flagsSize = sizeof(sFlags);
   return cwNoErr;
}

Originally, the fourth line of flags was just linkMultiTargAware; since we're not supporting Windows or disassembling, and since we are building a post-linker, we've changed that to isPostLinker | cantDisassemble.

The next important change concerns the routine CWPlugin_GetTargetList, shown in Listing 11. As you can see, we indicate that our post-linker applies only to applications built for the Windows operating system, since there is no need to RezWack Macintosh applications.

Listing 11: Specifying the plug-in targets

CWPlugin_GetTargetList
CWPLUGIN_ENTRY(CWPlugin_GetTargetList)
            (const CWTargetList** targetList)
{
   static CWDataType sCPU = targetCPUAny;
   static CWDataType sOS = targetOSWindows;
   static CWTargetList sTargetList = 
            {kCurrentCWTargetListVersion, 1, &sCPU, 1, &sOS};
   *targetList = &sTargetList;
   return cwNoErr;
}

The main function of a CodeWarrior plug-in is largely a dispatcher that calls other functions in the plug-in in response to various requests it receives. Here we need to make just one change to the sample plug-in code, namely to return an error if our plug-in is asked to disassemble some object code:

case reqDisassemble:
   /* disassemble object code for a given project file */
   result = cwErrRequestFailed;
   break;

The only request we really want to handle is the reqLink request; in response to this request, we call the Link function defined in Listing 12.

Listing 12: Handling a link request

Link
static CWResult Link(CWPluginContext context)

{
   CWTargetInfo      targetInfo;
   CWResult            err;
   FSSpec               fileSpec;
   // get the current linker target
   err = CWGetTargetInfo(context, &targetInfo);
   // get an FSSpec from the CWFileSpec
   ConvertCWFileSpecToFSSpec(&targetInfo.outfile, 
            &fileSpec);
   // add Mac resources to linker target to create final Windows executable
   if (err == cwNoErr)
      err = CreateRezWackedFileFromBinary(context, 
            &fileSpec);
   return (err);
}

CWGetTargetInfo returns information about the link target (that is, the file created by the CodeWarrior linker); we can extract a file system specification from that information using the utility function ConvertCWFileSpecToFSSpec. Then we pass that specification to CreateRezWackedFileFromBinary. Finally we are on familiar-looking ground once again. The definition of CreateRezWackedFileFromBinary is shown in Listing 13.

Listing 13: Getting names for the resource and wacked files

CreateRezWackedFileFromBinary
OSErr CreateRezWackedFileFromBinary 
         (CWPluginContext context, FSSpecPtr theBinFSSpecPtr)
{
   FSSpec            myResSpec;
   FSSpec            myExeSpec;
   CWMemHandle   prefsHand;
   SamplePref      prefsData;
   SamplePref      *prefsPtr;
   short            errMsgNum;
   CWResult         err;
   OSErr            myErr = paramErr;
   if (theBinFSSpecPtr == NULL)
      goto bail;
   // install a default name for the output file
   myExeSpec = *theBinFSSpecPtr;   
   myExeSpec.name[myExeSpec.name[0] - 2] = 'e';
   myExeSpec.name[myExeSpec.name[0] - 1] = 'x';
   myExeSpec.name[myExeSpec.name[0] - 0] = 'e';
   // install a default name for the resource file
   myResSpec = *theBinFSSpecPtr;
   myResSpec.name[myResSpec.name[0] - 2] = 'q';
   myResSpec.name[myResSpec.name[0] - 1] = 't';
   myResSpec.name[myResSpec.name[0] - 0] = 'r';
   // load the panel prefs and get the specified names for the resource and output files
   err = CWGetNamedPreferences(context, kSamplePanelName, 
            &prefsHand);
   if (err == cwNoErr) {
      err = CWLockMemHandle(context, prefsHand, false, 
            (void**)&prefsPtr);
      if (err == cwNoErr) {
         prefsData = *prefsPtr;
         myExeSpec.name[0] = strlen(prefsData.outfile);
         BlockMoveData(prefsData.outfile, myExeSpec.name + 1, 
            myExeSpec.name[0]);
         myResSpec.name[0] = strlen(prefsData.resfile);
         BlockMoveData(prefsData.resfile, myResSpec.name + 1, 
            myResSpec.name[0]);
         CWUnlockMemHandle(context, prefsHand);
      }
   }   
   myErr = RezWackWinBinaryAndMacResFile(theBinFSSpecPtr, 
                  &myResSpec, &myExeSpec, &errMsgNum);
   if (myErr != noErr)
      ReportError(context, errMsgNum);
bail:
   return(myErr);
}

The central portion of CreateRezWackedFileFromBinary retrieves the names of the resource file and the desired output file from our custom settings panel; then it calls RezWackWinBinaryAndMacResFile (which is largely just a renamed version of QTRW_RezWackWinBinaryAndMacResFile). In theory, we could omit the code that retrieves the resource file name and the output file name and just use hard-coded extensions, like we did in our RezWack PPC droplet. This would relieve us of having to write a settings panel plug-in, but it would provide less flexibility in naming things. Let's do things the right way, even if it means a bit of extra work.

Believe it or not, that's all we need to change in the sample linker to make our RezWack post-linker. We finish up by building the post-linker plug-in and installing it into the appropriate folder in the CodeWarrior plug-in directory. The next time we launch CodeWarrior, we'll be able to select our post-linker in the Target Settings panel of a Windows application project, as shown in Figure 11.


Figure 11: The post-linker menu with our RezWack plug-in

Handling Post-Link Errors

You may have noticed, in Listing 13, that RezWackWinBinaryAndMacResFile returns an error code through the errMsgNum parameter. For instance, if the attempt to open the specified resource file fails, then RezWackWinBinaryAndMacResFile executes this code:

if (myErr != noErr) {
   *errMsgNum = kOpenResError;
   goto bail;
}

If CreateRezWackedFileFromBinary sees that errMsgNum is non-zero, then it calls a function ReportError to display an error message to the user; a typical error message is shown in Figure 12.


Figure 12: A post-linking error message

The kOpenResError constant is defined in our header file CWRezWack.h; here's the complete list of error-related constants defined there:

#define   kExeCreateError               1
#define   kExeCreateErrorL2            2
#define   kOpenExeError               3
#define   kOpenExeErrorL2               4
#define   kOpenResError               5
#define   kOpenResErrorL2               6
#define   kOpenBinError               7
#define   kOpenBinErrorL2               8
#define   kGetMemError                  9
#define   kGetMemErrorL2               10
#define   kWriteExeError               11
#define   kWriteExeErrorL2            12

These are simply indices into a resource of type 'STR#' that is contained in the file CWRezWack.rsrc. Notice that each error has two corresponding string resources (for instance, kOpenResError and kOpenResErrorL2). These two strings provide the messages on the two lines of each error message in the "Errors & Warnings" window (for instance, "An error occurred while trying to open the resource file." and "Make sure it has the name specified in the RezWack panel.").

Listing 14 shows our definition of ReportError. The key ingredient here is the CWReportMessage function, which takes two strings and displays them to the user in the "Errors & Warnings" window.

Listing 14: Reporting a post-linking error to the user

ReportError
void ReportError (CWPluginContext context, short errMsgNum)
{
   Str255         pErrorMsg;
   char            cErrorMsgL1[256];
   char            cErrorMsgL2[256];
   GetIndString(pErrorMsg, kErrorStrID, errMsgNum);
   if (pErrorMsg[0] != 0)
   {
      BlockMoveData(&pErrorMsg[1], &cErrorMsgL1, pErrorMsg[0]);
      cErrorMsgL1[pErrorMsg[0]] = 0;
   }
   GetIndString(pErrorMsg, kErrorStrID, errMsgNum + 1);
   if (pErrorMsg[0] != 0)
   {
      BlockMoveData(&pErrorMsg[1], &cErrorMsgL2, pErrorMsg[0]);
      cErrorMsgL2[pErrorMsg[0]] = 0;
   }
   CWReportMessage(context, NULL, (char*)&cErrorMsgL1, 
            (char*)&cErrorMsgL2, messagetypeError, 0);
}

Writing a Settings Panel Plug-In

The final thing we need to do is construct our settings panel plug-in (also called a preference panel plug-in), which displays the panel we saw earlier (Figure 9) and communicates the settings in that panel to our post-linker when it calls CWGetNamedPreferences (as in Listing 13). Once again, we'll clone the sample settings panel project and rename things appropriately, to obtain the project window shown in Figure 13.


Figure 13: The RezWack settings panel project window

The layout of the settings panel is determined by a resource of type 'PPob' in the resource file RezWack.rsrc. Figure 14 shows the RezWack panel as it appears in the Constructor application.


Figure 14: The RezWack settings panel in Constructor

Our RezWack settings panel contains only five items, which we can identify in our code using these constants:

enum {
   kStaticText               = 1,
   kResFileLabel,
   kResFileEditBox,
   kOutputFileLabel,
   kOutputFileEditBox
};

Most of the changes required in the sample settings panel plug-in code involve removing unneeded routines, which I won't discuss further. The two key functions are the PutData and GetData routines, which transfer data between the on-screen settings panel and an in-memory handle of settings data. For our RezWack settings panel, the data in memory has this structure:

typedef struct SamplePref {
   short      version;
   char         outfile[kFileNameSize];
   char         resfile[kFileNameSize];
} SamplePref, **SamplePrefHandle;

(Take a look back at Listing 13 to see how we use the outfile and resfile fields when building a wacked file.)

Listing 15 shows our version of the PutData function, which copies settings information from the handle of settings data to the settings panel.

Listing 15: Copying settings data into the settings panel

PutData
static CWResult PutData (CWPluginContext context)
{
   CWResult         result = cwNoErr;
   CWMemHandle   memHandle = NULL;
   SamplePref      thePreferences;
   try
   {
      // Get a pointer to the current prefs
      result = (GetThePreferences(context, &thePreferences));
      THROW_IF_ERR(result);
      // Stuff data into the preference dialog
      CWPanelSetItemText (context, kOutputFileEditBox, 
               thePreferences.outfile);
      CWPanelSetItemText (context, kResFileEditBox, 
               thePreferences.resfile);
   }
   catch (CWResult thisErr)
   {
      result = thisErr;
   }
   
   // Relinquish our pointer to the prefs data
   if (memHandle)
      CWUnlockMemHandle(context, memHandle); // error is ignored
   return (result);
}

And Listing 16 shows our version of the GetData function, which copies data from the panel to the handle of settings data.

Listing 16: Copying settings data out of the settings panel

GetData
static CWResult GetData(CWPluginContext context)
{
   CWResult            result = cwNoErr;
   CWMemHandle      memHandle = NULL;
   SamplePref         thePreferences;   // local copy of preference data
   char *             outname = (char *)malloc(255);
   char *             resname = (char *)malloc(255);
   short               i, j;
   try
   {
      // Get a pointer to the current prefs
      result = (GetThePreferences(context, &thePreferences));
      THROW_IF_ERR(result);
      // Stuff dialog values into the current prefs
      result = CWPanelGetItemText(context, 
            kOutputFileEditBox, outname, 
            sizeof(thePreferences.outfile));
      THROW_IF_ERR(result);
      // apparently non-printing characters can squeeze their way into the dialog box,
      // so winnow them out...
      for (i = 0, j = 0; i < strlen(outname); i++)
         if (isprint(outname[i]))
            thePreferences.outfile[j++] = outname[i];
      thePreferences.outfile[j] = 0;
      result = CWPanelGetItemText(context, kResFileEditBox, 
         resname, sizeof(thePreferences.resfile));
      THROW_IF_ERR(result);
      // apparently non-printing characters can squeeze their way into the dialog box,
      // so winnow them out...
      for (i = 0, j = 0; i < strlen(resname); i++)
         if (isprint(resname[i]))
            thePreferences.resfile[j++] = resname[i];
      thePreferences.resfile[j] = 0;
      // Now update the "real" prefs data
      result = (PutThePreferences(context, &thePreferences));
      THROW_IF_ERR(result);
   }
   catch (CWResult thisErr)
   {
      result = thisErr;
   }
   // Relinquish our pointer to the prefs data
   free(outname);
   free(resname);
   return result;
}

I have found that non-printing characters can sometimes make their way into the edit-text boxes in the settings panel, so I explicitly look for them and remove them from the file names typed by the user.

The settings panel plug-in code contains a handful of other functions that save preferences on disk, restore preferences to their previous settings, and so forth. I'll leave the inspection of those routines to the interested reader. For the rest of us, we can finish up by building the settings panel plug-in and installing it into the correct folder in the CodeWarrior installation.

Conclusion

In this article, we've seen how to embed Macintosh resources into a Windows executable file, whether we want to develop our applications on Windows or Macintosh computers. This resource embedding allows us to take advantage of the support provided by the QuickTime Media Layer for those parts of the Macintosh User Interface Toolbox and the Macintosh Operating System that rely heavily on resources, including the Dialog Manager, the Window Manager, the Control Manager, and of course the Resource Manager. This in turn makes it easy to write our code once and deliver it on several platforms.

References

You can download the CodeWarrior plug-in SDK from http://www.metrowerks.com/MW/Support/API.htm.


Tim Monroe is a member of the QuickTime engineering team. You can contact him at monroe@apple.com. The views expressed here are not necessarily shared by his employer.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Ableton Live 11.3.11 - Record music usin...
Ableton Live lets you create and record music on your Mac. Use digital instruments, pre-recorded sounds, and sampled loops to arrange, produce, and perform your music like never before. Ableton Live... Read more
Affinity Photo 2.2.0 - Digital editing f...
Affinity Photo - redefines the boundaries for professional photo editing software for the Mac. With a meticulous focus on workflow it offers sophisticated tools for enhancing, editing and retouching... Read more
SpamSieve 3.0 - Robust spam filter for m...
SpamSieve is a robust spam filter for major email clients that uses powerful Bayesian spam filtering. SpamSieve understands what your spam looks like in order to block it all, but also learns what... Read more
WhatsApp 2.2338.12 - Desktop client for...
WhatsApp is the desktop client for WhatsApp Messenger, a cross-platform mobile messaging app which allows you to exchange messages without having to pay for SMS. WhatsApp Messenger is available for... Read more
Fantastical 3.8.2 - Create calendar even...
Fantastical is the Mac calendar you'll actually enjoy using. Creating an event with Fantastical is quick, easy, and fun: Open Fantastical with a single click or keystroke Type in your event details... Read more
iShowU Instant 1.4.14 - Full-featured sc...
iShowU Instant gives you real-time screen recording like you've never seen before! It is the fastest, most feature-filled real-time screen capture tool from shinywhitebox yet. All of the features you... Read more
Geekbench 6.2.0 - Measure processor and...
Geekbench provides a comprehensive set of benchmarks engineered to quickly and accurately measure processor and memory performance. Designed to make benchmarks easy to run and easy to understand,... Read more
Quicken 7.2.3 - Complete personal financ...
Quicken makes managing your money easier than ever. Whether paying bills, upgrading from Windows, enjoying more reliable downloads, or getting expert product help, Quicken's new and improved features... Read more
EtreCheckPro 6.8.2 - For troubleshooting...
EtreCheck is an app that displays the important details of your system configuration and allow you to copy that information to the Clipboard. It is meant to be used with Apple Support Communities to... Read more
iMazing 2.17.7 - Complete iOS device man...
iMazing is the world’s favourite iOS device manager for Mac and PC. Millions of users every year leverage its powerful capabilities to make the most of their personal or business iPhone and iPad.... Read more

Latest Forum Discussions

See All

‘Junkworld’ Is Out Now As This Week’s Ne...
Epic post-apocalyptic tower-defense experience Junkworld () from Ironhide Games is out now on Apple Arcade worldwide. We’ve been covering it for a while now, and even through its soft launches before, but it has returned as an Apple Arcade... | Read more »
Motorsport legends NASCAR announce an up...
NASCAR often gets a bad reputation outside of America, but there is a certain charm to it with its close side-by-side action and its focus on pure speed, but it never managed to really massively break out internationally. Now, there's a chance... | Read more »
Skullgirls Mobile Version 6.0 Update Rel...
I’ve been covering Marie’s upcoming release from Hidden Variable in Skullgirls Mobile (Free) for a while now across the announcement, gameplay | Read more »
Amanita Design Is Hosting a 20th Anniver...
Amanita Design is celebrating its 20th anniversary (wow I’m old!) with a massive discount across its catalogue on iOS, Android, and Steam for two weeks. The announcement mentions up to 85% off on the games, and it looks like the mobile games that... | Read more »
SwitchArcade Round-Up: ‘Operation Wolf R...
Hello gentle readers, and welcome to the SwitchArcade Round-Up for September 21st, 2023. I got back from the Tokyo Game Show at 8 PM, got to the office here at 9:30 PM, and it is presently 11:30 PM. I’ve done what I can today, and I hope you enjoy... | Read more »
Massive “Dark Rebirth” Update Launches f...
It’s been a couple of months since we last checked in on Diablo Immortal and in that time the game has been doing what it’s been doing since its release in June of last year: Bringing out new seasons with new content and features. | Read more »
‘Samba De Amigo Party-To-Go’ Apple Arcad...
SEGA recently released Samba de Amigo: Party-To-Go () on Apple Arcade and Samba de Amigo: Party Central on Nintendo Switch worldwide as the first new entries in the series in ages. | Read more »
The “Clan of the Eagle” DLC Now Availabl...
Following the last paid DLC and free updates for the game, Playdigious just released a new DLC pack for Northgard ($5.99) on mobile. Today’s new DLC is the “Clan of the Eagle" pack that is available on both iOS and Android for $2.99. | Read more »
Let fly the birds of war as a new Clan d...
Name the most Norse bird you can think of, then give it a twist because Playdigious is introducing not the Raven clan, mostly because they already exist, but the Clan of the Eagle in Northgard’s latest DLC. If you find gathering resources a... | Read more »
Out Now: ‘Ghost Detective’, ‘Thunder Ray...
Each and every day new mobile games are hitting the App Store, and so each week we put together a big old list of all the best new releases of the past seven days. Back in the day the App Store would showcase the same games for a week, and then... | Read more »

Price Scanner via MacPrices.net

Apple AirPods 2 with USB-C now in stock and o...
Amazon has Apple’s 2023 AirPods Pro with USB-C now in stock and on sale for $199.99 including free shipping. Their price is $50 off MSRP, and it’s currently the lowest price available for new AirPods... Read more
New low prices: Apple’s 15″ M2 MacBook Airs w...
Amazon has 15″ MacBook Airs with M2 CPUs and 512GB of storage in stock and on sale for $1249 shipped. That’s $250 off Apple’s MSRP, and it’s the lowest price available for these M2-powered MacBook... Read more
New low price: Clearance 16″ Apple MacBook Pr...
B&H Photo has clearance 16″ M1 Max MacBook Pros, 10-core CPU/32-core GPU/1TB SSD/Space Gray or Silver, in stock today for $2399 including free 1-2 day delivery to most US addresses. Their price... Read more
Switch to Red Pocket Mobile and get a new iPh...
Red Pocket Mobile has new Apple iPhone 15 and 15 Pro models on sale for $300 off MSRP when you switch and open up a new line of service. Red Pocket Mobile is a nationwide service using all the major... Read more
Apple continues to offer a $350 discount on 2...
Apple has Studio Display models available in their Certified Refurbished store for up to $350 off MSRP. Each display comes with Apple’s one-year warranty, with new glass and a case, and ships free.... Read more
Apple’s 16-inch MacBook Pros with M2 Pro CPUs...
Amazon is offering a $250 discount on new Apple 16-inch M2 Pro MacBook Pros for a limited time. Their prices are currently the lowest available for these models from any Apple retailer: – 16″ MacBook... Read more
Closeout Sale: Apple Watch Ultra with Green A...
Adorama haș the Apple Watch Ultra with a Green Alpine Loop on clearance sale for $699 including free shipping. Their price is $100 off original MSRP, and it’s the lowest price we’ve seen for an Apple... Read more
Use this promo code at Verizon to take $150 o...
Verizon is offering a $150 discount on cellular-capable Apple Watch Series 9 and Ultra 2 models for a limited time. Use code WATCH150 at checkout to take advantage of this offer. The fine print: “Up... Read more
New low price: Apple’s 10th generation iPads...
B&H Photo has the 10th generation 64GB WiFi iPad (Blue and Silver colors) in stock and on sale for $379 for a limited time. B&H’s price is $70 off Apple’s MSRP, and it’s the lowest price... Read more
14″ M1 Pro MacBook Pros still available at Ap...
Apple continues to stock Certified Refurbished standard-configuration 14″ MacBook Pros with M1 Pro CPUs for as much as $570 off original MSRP, with models available starting at $1539. Each model... Read more

Jobs Board

Omnichannel Associate - *Apple* Blossom Mal...
Omnichannel Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Read more
Cashier - *Apple* Blossom Mall - JCPenney (...
Cashier - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Blossom Mall Read more
Operations Associate - *Apple* Blossom Mall...
Operations Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Read more
Retail Key Holder- *Apple* Blossom Mall - Ba...
Retail Key Holder- APPLE BLOSSOM MALL Brand: Bath & Body Works Location: Winchester, VA, US Location Type: On-site Job ID: 03YM1 Job Area: Store: Sales and Support Read more
Omnichannel Associate - *Apple* Blossom Mal...
Omnichannel Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.