Monday, October 19, 2009

Section 14.7.  Producing a Universal Binary










14.7. Producing a Universal Binary


The other use for an SDK in Xcode is in producing a universal binary, a single, linked executable file for the Mach-O linker that contains machine code for both the PowerPC and the Intel architecture.


Linear is an undemanding application and would probably run perfectly well under Rosetta, the PowerPC translation system for running PPC software on Intel Macintoshes. But pride, if nothing else, drives us to add the Intel code.



14.7.1. Auditing for Portability


In adding an Intel version to Linear, the first thing to do is examine the application for anything that would make it behave differently on an Intel processor, such as use of


  • PowerPC assembly

  • Altivec (Velocity Engine) instructions

  • Binary numerics in a context that might be shared with other devices, such as a network, low-level file system structures, or Macintoshes of other architectures

  • UTF-16 or UTF-32 encoding for Unicode text, without the use of a byte-order marker (BOM)

  • Arrays of char in unions to examine the contents of other data types

  • Bitfield struct or union members in cross-platform data

  • bool, which is 32 bits in gcc on the PowerPC but 8-bits on Intel (and in Code-Warrior)


The first two points of incompatibility should be obvious: You can't run PowerPC standard or extended code on a processor with a different architecture. You can guard such code in C-family source code by checking for the target processor:


/*  Processor symbols begin and end with two underscores */
#if _ _ppc_ _
/* Altivec or PPC assembly here */
#end
#if _ _i386_ _
/* MMX/SSE2 or Pentium assembly here * /
#end


I assume that you're using assembly for a good reasonyou've measured the equivalent C code in Shark and discovered a bottleneck in exactly that placeso I'll spare you the reminder to consider using plain C code for both cases.


The next three categories of compatibility arise from the fact that the PowerPC stores numbers in memory so that the lowest-addressed memory gets the most-significant byte (big-endian), and Intel processors store numbers so that the lowest-addressed memory gets the least-significant byte (little-endian). Bit patterns that represent identical numbers on the two architectures are not identical when considered as a sequence of bytes.


Network numbersaddresses, port numbersare big-endian, and Intel applications have to reverse any numbers that pass between net protocols and internal use. Numbers stored as binary images in files can be big-or little-endian, but a decision has to be made as to which, and the opposite platform has to convert at the read/write boundary.


Multibyte Unicode encodings, being strings of 2-or 4-byte integers, are simply a special case of the endianness problem. Writers of such files should write the byte-order mark character, 0xfeff (0x0000feff in UTF-32), at the beginning of the file to inform readers of endianness. If a Latin-text multibyte Unicode file comes out in Chinese when opened on a Macintosh with the opposite architecture, the BOM was either omitted or ignored.



14.7.2. Auditing Linear


When we examine Linear for Intel-porting issues, we find nothing much. There is no reliance on machine code or processor features. Linear does a lot of numeric computation internally, but internal use is not an issue. Similarly, passing the numbers to and from the user interface is not an issue: Although reading and displaying numeric strings involve an external interface, it's to a string format, not a binary format. The Macintosh frameworks themselves are of the same endianness as the machine they run on, so they're of the same endianness as our numerics; no byte-swapping issue there.


What about our storage formats? We have two. The plist format has no byte-swapping issues, because the numbers are translated into XML character data; it's not a binary format.


That leaves the reading and writing of .linear document files. We use NSKeyed-Archiver and NSKeyedUnarchiver to reduce our data to the file format. Apple assures us that anything that passes through the Cocoa archivers is byte-order safe. A .linear file written on a PPC Mac will be readable on an Intel Mac.


Just for illustration purposes, suppose that we did have a raw-binary file format for Linear data files. We would choose whether numerics should be stored in the file in big-or little-endian format. If our PowerPC application had a binary format in the first place, it probably saved its data as a direct binary image, so the choice of big-endian order is already made; the conversion task is simply to change the binary reading and writing so that it behaves well regardless of the architecture it runs on.


Let's imagine our binary-I/O code in more, though certainly not complete, detail:


 #import <NSFoundation/NSByteOrder.h>

// All numerics in disk storage are big-endian

struct PointBinFmt {
NSSwappedDouble x;
NSSwappedDouble y;
};
typedef struct PointBinFmt PointBinFmt, *PointBinFmtPtr;

struct LinearBinaryFmt {
NSSwappedDouble slope;
NSSwappedDouble intercept;
NSSwappedDouble correlation;
unsigned long pointCount;
PointBinFmt points[0];
};
typedef struct LinearBinFmt LinearBinFmt, *LinearBinFmtPtr;

inline static unsigned
LinearBinFmtSize(LinearBinFmtPtr anLBF) {
return sizeof(LinearBinFmt)
+ anLBF->pointCount * sizeof(PointBinFmt);
}

@interface DataPoint (BinaryFormat)
- (id) initWithBinary: (const PointBinFmtPtr) binary
{
x = NSSwapBigDoubleToHost(binary->x);
y = NSSwapBigDoubleToHost(binary->y);
return self;
}
- (void) fillBinary: (PointBinFmtPtr) binary
{
binary->x = NSSwapHostDoubleToBig(x);
binary->y = NSSwapHostDoubleToBig(y);
}
@end

@interface Regression (BinaryFormat)
- (LinearBinFmtPtr) allocBinaryPtr
{
LinearBinFmtPtr retval = malloc(sizeof(LinearBinFmt)
+ [dataPoints count] *
sizeof(PointBinFmt));
retval->slope = NSSwapHostDoubleToBig(slope);
.
.
.
retval->pointCount = NSSwapHostLongToBig([dataPoints count]);
.
.
.
}


We set up structs to lay out our file format and add a BinaryFormat category to both DataPoint and Regression to translate between those classes and the file format. The data format structs use NSSwappedDouble in place of double for floating-point values. Apple's documentation speaks of NSSwappedDouble and its partner NSSwappedFloat as "canonical representations" of real numbers; in practice, they are, respectively, long long and long ints containing the same bit pattern as a swapped or unswapped floating-point number. They are the recommended storage type for floats and doubles.


The Foundation header NSByteOrder.h contains in-line function definitions for swapping numeric data types between the processor's native format and either big-or little-endian format. Versions of these functions are guarded with #if _ _BIG_ENDIAN_ _ or #if _ _LITTLE_ENDIAN_ _ so that attempts to swap in the native ordering are replaced with simple assignments. The following statement is equivalent to x = binary->x; on the PowerPC but on Intel reverses the byte order of binary->x before the assignment:


 x = NSSwapBigDoubleToHost(binary->x);



If you're using Core Foundation instead of Cocoa, the equivalent functions can be found in CFByteOrder.h; as with the NS* versions, these functions are optimized away at compile time if no swapping is necessary.



Making the BinaryFormat categories byte-order safe is a matter of identifying the places in the code where swappable data becomes shareable. In our case, it's when the data is committed to the file format. At that point, we interpose the appropriate swapping function. On writing, that would be a function that begins with NSSwapHost and ends ToBig, as we've decided that the file format will be big-endian. On reading, the swap-function names would begin with NSSwapBig and end with ToHost.



14.7.3. Building Universal


To produce a universal executable, you must tell the Xcode tools to compile and link Linear once for each target architecture. Then you have to link both images to Mac OS X frameworks.


Adding the second architecture is easy. Double-click the project icon at the top of the Groups & Files list. Switch to the Build tab of the Project Info window. In the top pop-up menu, select the Release configuration. Select Architectures in the second pop-up, or type arch in the search field until you see the Architectures setting. This should be, by default, $(NATIVE_ARCH), which means whatever processor Xcode is running on.


Click the Edit button below the settings list; a sheet appears with checkboxes for PowerPC and Intel. See Figure 14.5. Check both, and click OK. Now all future Release builds will include two compilation and linkage passes, one for each processor.



Figure 14.5. Setting the architecture for a project. Select the Release configuration in the Project Info window to ensure that all targets inherit the setting. Check each architecture you want to run on.








Make the change only in the Release build configuration. You can't use the other architecture's debug output, so producing it is a waste of time. Do this on the project Release configuration, not on the target, so that both the targets that go into the Linear applicationthe application and the statistics frameworkinherit the new setting.



The Release build of an application has to be linked against frameworks that supply its runtime library, Carbon, Cocoa, and every other service the system provides. The resident Mac OS X libraries you get when you compile without an SDK will not do; they don't have entry points for Intel code. Fortunately, we have, for the production of universal binaries, a Universal edition of the SDK for Mac OS X 10.4. This edition contains stub library entry points for both PowerPC and Intel.


So open the Project Info window again, if you've closed it. In the General tab, select cross-development, using Mac OS X 10.4 (Universal). Now you will be able to build linear without complaints from the linker about missing all kinds of fundamental symbols. Select Project Set Active Build Configuration . Release, make sure that Linear is the current target, and build. The result should be a version of Linear that will run natively on both Intel and PowerPC.



14.7.4. Mixing SDKs


If you want your application to run on an Intel Macintosh, you are, in late 2005, committed to compiling it against an SDK for Mac OS X 10.4. There's no other choice. It may be that you want the PowerPC side of your application to target an earlier release of Mac OS X. This is possible.


When you select an SDK in the General tab of the Project Info window, you are setting the SDKROOT variable in Xcode's build system. The contents of this variable are prepended to the search paths for frameworks, headers, and libraries in the course of a build. Xcode also observes processor-specific SDKROOTs, SDKROOT_ppc, and SDKROOT_i386, if those are set.


Once again, open the Project Info window, select the Build tab, and choose the Release build configuration. Click the + button twice to add two custom settings, named SDKROOT_i386 and SDKROOT_ppc. Type in the path to the desired SDK in the right-hand column for each (for example, /Developer/SDKs/MacOSX10.4u.sdk; or, you could drag the SDK folder from the Finder into the right-hand column, and the correct path would appear. (See Figure 14.6) Release builds from then on would target Mac OS X 10.4 on the Intel side but Mac OS X 10.3.9 on PowerPC.



Figure 14.6. Setting separate SDKs for Intel and PowerPC builds. Add two settings, SDKROOT_i386 and SDKROOT_ppc, to the project Release configuration. The setting should be the path to the SDK's directory in /Developer/SDKs, and dragging the SDK folder from the Finder into the setting is a convenient way to enter the path.







Things get trickier if you want your PowerPC code to run on versions of Mac OS X earlier than 10.3.9. Those earlier versions lack runtime libraries required by applications built with gcc 4, but gcc 4 is required for Intel builds. Your best solution is to create a separate target for your PowerPC code, use the Rules tab of the target Info window to set the compiler to gcc 3.3, and build separate binaries for Intel and PowerPC. When you have the two binaries, you can combine them using the lipo command line tool, which is documented in a man page.












No comments:

Post a Comment