Developing with Objective Pascal
Part 10: Creating a Multi-Architecture Static Library (Pascal)
Contents
Introduction
Requirements
Creating the Library Project
Wrapping the Pascal Class
Wrapping the Pascal Methods and Properties
Wrapping Tips
Testing the Wrapper Class
Notes
Introduction
Part 8 describes how to compile Objective C
classes into a static library that you can use in an Objective Pascal app for
iOS. These notes describe how to create an Objective C wrapper for a Pascal class
and compile it into a static library for use in an Objective C app for iOS.
When would it be appropriate to use Pascal in an Objective C app? Several
typical scenarios suggest themselves:
- You want to write your iOS app's user interface in Objective C, but some
or all of the non-UI portion of the app in Pascal.
- You need to write a portion of your app in Objective C in order to utilize
features of Xcode, Interface Builder or Objective C that are not available
or are difficult to use with Pascal (Automatic Reference Counting, Core Data,
Storyboard files, C blocks, etc.).
- You want to use existing Pascal code in a new app where the
choice of language for the main body of the app is not yours to make.
Requirements
- Xcode 4.2 or later.
- Custom Pascal templates for Xcode, available
here.
Unzip and place the Objective Pascal folder below
/Users/yourname/Library/Developer/Xcode/Templates (you will need to create the
Templates folder if it doesn't exist).
- Free Pascal 2.6.2 compilers for i386 and ARM. Installers can be
downloaded from here.
- iPhoneAll units compiled for both Simulator and ARM. Since the ARM object
files (.o) included with FPC 2.6.2 contain both armv6 and armv7 code, make sure
you compile the iPhoneAll unit for both armv6 and armv7 using the compile-arm.sh
script included with the framework header parser
here.
Creating the Library Project
Start by creating a project in Xcode 4 using the Objective Pascal project
template for a Cocoa Touch Static Library. (If you don't see Cocoa Touch
Static Library on the Objective Pascal tab for iOS, then you don't have
the templates installed correctly.)
As an example, we'll wrap the Pascal TStringList class, so enter the
following:
- Product Name - enter StringList here. As with other project
templates, what you enter here will be used to name the project folder.
The resulting compiled library will be named "lib" + product name + ".a".
For example, with StringList, the compiled library will be named
libStringList.a. If you plan to include more than one class in your library,
you'll probably want to use a more general name here. If you'll only be wrapping
a single class, the class name could be used as the project name too.
- Objective-C Class - enter PasStringList here.
To avoid naming conflicts, don't enter the name of your Pascal class. Instead,
since Objective C does not use the convention of a leading "T" for class names,
drop the Pascal class's "T" and start with that for the ObjC class name. To
ensure that your class name is unique, it's a good idea to add a prefix of your
own like we did here.
Once you've saved your project, take a look at what the template has
created for you:
- A pair of Objective C files (PasStringList.h and PasStringList.m) and
a corresponding Pascal file (PasStringListU.pas). These files define and
implement a simple subclass of NSObject named PasStringList; you'll be adding
to this class below.
- Settings for the Free Pascal compilers on the target's Build Settings
tab.
As with any Objective Pascal project for iOS, if your iPhoneAll unit's files
are not in the expected locations, you'll need to change the path in the
FPC_ARM_UNITS and FPC_SIM_UNITS settings.
You can copy your Pascal class's source files into the project folder so the
Pascal compiler will find them. Or you can tell the compiler where they're
located using the -Fu switch that you can add to the FPC_COMMON setting.
For this example, we're only wrapping a runtime library class, which the
compiler can find on its own, so we don't need to use the -Fu switch.
- Run Script on the target's Build Phases tab. When you do a build for
a platform, this script creates an .a (archive) file from the compiled
.o (object) files for the platform's architecture(s). Once you've
successfully compiled for both the Simulator and iOS Device, the script
will combine both platforms' .a files into a single multi-architecture .a file
in the top level of your project folder. This .a file is what you will link
into any Objective C app.
Wrapping the Pascal Class
Your wrapper class needs to declare and create an instance of
your Pascal class. This object is what the Pascal code will operate on.
(Note: This technique is sometimes referred to as object composition.)
In our example, declare and create a TStringList object. Your code should
look like this, where the additions you need to make are in bold.
Note that you can edit the Pascal file in an external text editor such as
TextWrangler
that provides Pascal syntax highlighting.
unit PasStringListU;
{$modeswitch ObjectiveC1}
interface
uses
iPhoneAll, Classes;
type
PasStringList = objcclass(NSObject)
private
FStrList : TStringList;
public
function init : id; override;
procedure dealloc; override;
end;
implementation
function PasStringList.init : id;
begin
Result := inherited init;
if Assigned(Result) then
begin
FStrList := TStringList.Create;
end;
end;
procedure PasStringList.dealloc;
begin
FStrList.Free;
inherited dealloc;
end;
end.
Wrapping the Pascal Methods and Properties
For each method or property in your Pascal class that you want to be able
to use in an ObjC app, perform the following steps:
- Add a method declaration to the ObjC .h file.
- Copy this method declaration to the ObjC .m file and add a dummy code block
to it.
- Add the equivalent Pascal method to the .pas file's interface and
implementation sections, then implement the method.
In our example library, we'll wrap the TStringList Add, Count and Strings
members, as follows:
- Add method declarations to the PasStringList.h header file.
For help with ObjC class and method syntax, see
Apple's introduction.
@interface PasStringList : NSObject
- (NSInteger) add:(NSString *)s;
- (NSInteger) count;
- (NSString *) strings:(NSInteger)index;
@end
- Add methods to the PasStringList.m source file. For each method, add a code
block and return a dummy value to eliminate Xcode warnings. It doesn't matter
what value you return since the ObjC code will not be used.
- (NSInteger) add:(NSString *)s;
{
return 0;
}
- (NSInteger) count;
{
return 0;
}
- (NSString *) strings:(NSInteger)index;
{
return NULL;
}
- Add methods to the PasStringListU.pas file. First declare the methods in
the PasStringList wrapper class:
function add(s: NSString) : NSInteger; message 'add:';
function count : NSInteger; message 'count';
function strings(index : NSInteger) : NSString; message 'strings:';
Then implement the methods:
function PasStringList.add(s: NSString) : NSInteger;
begin
Result := FStrList.Add(s.UTF8String);
end;
function PasStringList.count : NSInteger;
begin
Result := FStrList.Count;
end;
function PasStringList.strings(index : NSInteger) : NSString;
begin
Result := NSString.stringWithUTF8String(PChar(FStrList.Strings[index]));
end;
Wrapping Tips
- The example's methods do not include any error checking or
debugging code. Normally you'll want to include that sort of code while
developing your wrapper class and conditionally exclude it only when compiling
a final version of your library. Also, it's probably a good idea
to debug your Pascal class fully before you attempt to wrap it - then
you'll only have to deal with errors in the wrapper code, not in the
wrapped class as well.
- The example is a contrived one since normally you would never wrap a
runtime library class like this. Instead, in your ObjC app you would use
an equivalent iOS framework class (for example, NSArray or NSMutableArray).
- To help decide what types to pass with your wrapper methods, consult
the Foundation framework's
data types
and classes.
For example, many of the TStringList methods and properties take a signed
integer parameter. NSInteger looks like a good candidate to use.
With iOS, NSInteger is declared as the C "int" type (see NSObjCRuntime.h or
NSObjCRuntime.inc). Checking the FPC ctypes.pp file, we see that "cint" is
the same as "cint32" which is the same as the Pascal "longint" (and "Integer")
type.
- In converting to and from NSString, the Pascal code implicitly assumes
that TStringList works with AnsiString, which is currently the case with
Free Pascal 2.6. However, if a future version of FPC changes the TStringList
string type to UnicodeString, this code won't work. For more flexibility
in your code, you can use the NSHelpers unit,
which works with a variety of Pascal string types. For this example, you
could add NSHelpers to the uses statement and then make these substitutions:
In add: Result := FStrList.Add(NSStrToStr(s));
In strings: Result := StrToNSStr(FStrList.Strings[index]);
- The message string that follows each Objective Pascal method declaration is
composed by concatenating the Objective C method signature keywords, including
any colons. The number of colons in the message string is equal to the number
of parameters passed to the method.
Testing the Wrapper Class
Before testing the wrapper class, make sure you've done a Product | Clean
and Product | Build for both platforms to ensure that all architectures
reflect the same source code. Be sure to do this after making any
changes to the library source too.
To see what architectures are contained in the library, change to the
StringList project folder and use the file command:
file libStringList.a
libStringList.a: Mach-O universal binary with 3 architectures
libStringList.a (for architecture armv6): current ar archive random library
libStringList.a (for architecture armv7): current ar archive random library
libStringList.a (for architecture i386): current ar archive random library
Now create a simple Objective C app for iOS. For example, select the
Empty Application project template and enter the following:
- Product Name - enter TestStringList.
- Class Prefix - enter TSL.
- Device Family - select iPhone.
- Leave the check boxes unchecked.
Once you've saved the new project, do the following:
- Set the project's iOS Deployment Target to the earliest version of iOS
you want to support.
- Copy the library's header file (PasStringList.h) into the app's project
folder and add it to the project. A simple way to do that is to select the
yellow TestStringList in the Project navigator and choose File | Add Files.
Navigate to and select PasStringList.h and make sure the "Copy items"
box is checked, then click Add.
- On the target's Build Phases tab, under Link Binary With Libraries, click
the + button, then click the Add Other button and navigate to and select
the libStringList.a file.
- In TSLAppDelegate.m, below the #import directive, add this line:
#import "PasStringList.h"
- In TSLAppDelegate.m, add the following code just above the return
statement in the didFinishLaunchingWithOptions method:
PasStringList * strList = [[PasStringList alloc] init]; //allocate and initialize list object
[strList add:@"string 1"]; //add a string to list
[strList add:@"string 2"]; //add another
NSLog(@"%i", [strList count]); //output number of strings in list
NSLog(@"%@", [strList strings:0]); //output first string
NSLog(@"%@", [strList strings:1]); //output second string
[strList release]; //free list object
Normally you would not add code like this to didFinishLaunchingWithOptions,
but for this simple test app we'll dispense with a normal user interface and
just output everything to the debug window.
Note that if you had checked the Use Automatic Reference Counting box when
you created this test project, you would need to omit the strList release call
since it will be added for you automatically at compile time (more info
here).
Now select the Simulator scheme and choose Product | Run to compile and run the
app. If you don't see the debug window, choose View | Debug Area | Show
Debug Area. You should now see debug output that looks like this:
2011-12-28 18:28:22.296 TestStringList[3108:207] 2
2011-12-28 18:28:22.301 TestStringList[3108:207] string 1
2011-12-28 18:28:22.302 TestStringList[3108:207] string 2
To complete your testing, connect an iOS device, then compile and run the
app there too.
Notes
- How does all this work? If you look in the library project's main Pascal
file (under Supporting Files), you'll see that it's actually
a library, not a program. Normally the library keyword is used for creating
dynamic libraries (.dylib), not static libraries (.a), but in this case
we're just using it as a way to "collect" all of the dependent object
files required by the wrapper class.
If you look at the library project's FPC_COMMON build setting, you'll see that
it includes the -Cn switch, which causes the compiler to skip the link
stage, leaving behind file link.res, which includes a list of object
files that the linker would normally link into your binary. Furthermore, if you
look at the project's run script, you'll see that this list of object files is
extracted from link.res and fed to libtool, which creates the archive
(.a) for a single architecture. When compiling for an iOS device, libtool also
combines both the armv6 and armv7 archives into a two-architecture archive.
The run script's final step is to check that both platforms' archives exist and,
if so, combines them into a three-architecture archive in your library project's
folder.
- If you open link.res in a text editor, you'll see that the list of object
files is a long one. As a result, the archive file can be quite large since
it includes all of these .o files for three different architectures and at
least a couple of these files (system.o, sysutils.o) are fairly large. However,
when Xcode compiles an ObjC app for an iOS device, it includes the
-dead_strip switch, which causes the linker to remove unused functions and
data, thus reducing the size of the app's binary considerably.
- If you inspect the build log after compiling for an iOS device, you may
see one or more warnings like this:
ld: warning: 32-bit absolute address out of range
This is the result of an apparently harmless linker bug that is discussed
here.
- You can put multiple wrapper classes in the same library. With the
iOS system frameworks, the convention appears to be to define only one
class per .h file and you may want to adhere to that convention as well.
If so, just add a set of .h/.m/.pas files for each class, then add the class's
Pascal unit to the uses statement in the library project's main file (under
Supporting Files).
- If you need to distribute your library for someone else to use, be sure
to include both the archive file (for example, libStringList.a) and its header
file(s) (for example, StringList.h).
Copyright 2011 by Phil Hess.
macpgmr (at) fastermac (dot) net
First posted Dec. 28, 2011; last edited May 9, 2013.