In this article we’ll see how to migrate the Apple SquareCam demo project written in Objective-C to
Firemonkey.
The experience is very interesting since we
have to deal with some advanced Objective-C libraries: Grand central dispatch
for multitasking, Assets library for video input/output and Detector library
for face detection.
1.- Introduction
I started this project to test the power of Delphi XE5 in the iOS environment.I chose SquareCam because it is an Apple sample that uses some frameworks that are poorly covered in Delphi firemonkey.
To make easier to follow the translation process I’ll try to keep the Delphi code as close as possible to the original Objective-C code.
Following the techniques I describe in this paper, it will be easier for you the migration of existing Objective-C code to Delphi.
2.- Interfacing with Objective-C code
Before we can start translating Objective-C code into Delphi it is necessary to know the different techniques we have to interact with Objective-C code.2.1.- Linking to external C methods
If we need to access to an external C method which is defined in an external module (library or framework), we must define an procedure (or a function if it returns some value) with the external clause as follows:
procedure ProcName(Param1: ParamType; …); cdecl; external ‘libName’
name ‘externalName’ dependency ‘dependsOnLibrary’, ‘anotherLibrary’, …;
- cdecl describes the procedure calling convention and it is necessary to call any external procedures or functions created with any C compiler. Objective-C uses this calling convention by default.
- external ‘libName’ allows you to specify the name of the dynamic library, static library or framework where the procedure or function is defined.
- name externalName is an optional clause used to specify the name of the procedure as defined in the external library. If you don’t use this clause, the name for the external procedure is assumed to be the same as the name for the Delphi procedure.
- dependency ‘dependsOnLibrary’ is an optional clause that allows you to specify additional libraries which may be needed needed by the code.
{$IF DEFINED(IOS) and DEFINED(CPUARM)}
function DllGetDataSnapClassObject(const [REF] CLSID,
[REF] IID: TGUID; var Obj): HResult; cdecl;
external 'libmidas.a' name 'DllGetDataSnapClassObject'
dependency 'stdc++';
{$ENDIF IOS and CPUARM}
Note: On a Mac you can
dynamically load a dylib but in the iOS device you cannot use any custom dynamic
libraries, so you must statically link any custom library into your
application.
2.2.- Accessing external variables
Currently Delphi compiler doesn't allow the definition of external variables with the same syntax we used for procedures or functions.The only way to get the address of an external variable that is defined in an external library or framework is to use the GetProcAddress function.
This code shows how to use this function in iOS:
uses System.SysUtils;
const
LibName = ‘fullPathLibName’;
var
ModuleHandle: HMODULE;
function GetVarAddress(const VarName: String): Pointer;
begin
if ModuleHandle <> 0
then
Result :=
GetProcAddress(ModuleHandle, PWideChar(VarName))
else
Result := nil;
end;
initialization
ModuleHandle :=
LoadLibrary(PWideChar(LibName));
finalization
if ModuleHandle <> 0
then
FreeLibrary(ModuleHandle);
end.
Warning: Be careful not to
call the FreeLibrary procedure with the ModuleHandle until you are done with
the external variable or be sure that an external procedure of that external
module has been called at least once to keep it in memory.
2.3.- Wrapping Objective-C classes into Delphi.
All Objective-C classes inherit from NSObject. Apple has defined a large number of classes which are grouped into frameworks. You can think in a framework a special kind of library.
For example the AssetsLibrary framework
contains all the stuff needed to create and access multimedia content (photos,
video, etc).
Delphi represents these Objective-C classes by interfaces. One interface is needed for the Objective-C class methods and another for the instance methods.
You can explore the iOSapi.CocoaTypes unit to see how interfaces for NSObject are defined.
The name of the interface for the class methods is usually built appending the word Class to the name of the instance interface. For NSObject, the class methods interface will be named as NSObjectClass.
To use those interfaces a helper class is defined alongside the instance and class interfaces. The helper class must inherit from the generic class TOCGenericImport<C,T> which is defined in the Macapi.ObjectiveC unit. This class wraps up the process of importing the Objective-C class into Delphi.
The helper class has methods that allow you to create instances of the Objective-C object or to wrap an existing Objective-C object id (represented by a raw pointer) into a Delphi interface.
In Objective-C creating an object is a two stage process. First you call alloc to allocate the memory for the object. Then you call the initialization method (init by default) to initialize the object instance. This way of creating is so common that all objects in Objective-C have the new method that combines alloc and init in a single stage operation.
Delphi represents these Objective-C classes by interfaces. One interface is needed for the Objective-C class methods and another for the instance methods.
You can explore the iOSapi.CocoaTypes unit to see how interfaces for NSObject are defined.
The name of the interface for the class methods is usually built appending the word Class to the name of the instance interface. For NSObject, the class methods interface will be named as NSObjectClass.
To use those interfaces a helper class is defined alongside the instance and class interfaces. The helper class must inherit from the generic class TOCGenericImport<C,T> which is defined in the Macapi.ObjectiveC unit. This class wraps up the process of importing the Objective-C class into Delphi.
The helper class has methods that allow you to create instances of the Objective-C object or to wrap an existing Objective-C object id (represented by a raw pointer) into a Delphi interface.
In Objective-C creating an object is a two stage process. First you call alloc to allocate the memory for the object. Then you call the initialization method (init by default) to initialize the object instance. This way of creating is so common that all objects in Objective-C have the new method that combines alloc and init in a single stage operation.
TOCGenericImport class has equivalences
to those initialization methods:
- Alloc: for the Objective-C alloc.
- Create: For the Objective-C new (alloc and init).
var
View: UIView;
begin
View := TUIView.Alloc; //
Allocate memory for the Objective-C object.
View :=
TUIView.Wrap(View.initWithFrame(AFrame)); // Initializes the object.
…
end;
Custom initialization methods returns an
Objective-C object Id (not the Delphi interface that represents it). The only
way to get the Delphi interface is to call the helper class Wrap method.
Note: Be careful not to
pass a nil object id to the Wrap method or you’ll get application crashes.
To access the class methods of an
Objective-C class you use the OCClass
property of the helper class.
The following code sample shows how to call
to the class methods:
var
CurrentDevice: UIDevice;
begin
CurrentDevice :=
TUIDevice.Wrap(TUIDevice.OCClass.currentDevice);
…
end;
var
ObjectId: Pointer;
begin
ObjectId := (ObjInterface as
ILocalObject).GetObjectId;
…
end;
2.4 Objective-C methods
Delphi and Objective-C have a very different approach to method naming.In Objective-C the parameters become part of the method name to reduce the ambiguity and to help for the readability of the code.
To help to understand the implications of this syntax we are going to study some methods of the UIApplicationDelegate protocol. Firstly the method that triggers after the application has started up:
(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary
*)launchOptions
This is a function that returns a Boolean and takes two arguments; a reference to a UIApplication and a reference do a NSDictionary.
The Objective-C method name is application:didFinishLaunchingWithOptions: as you see the arguments are described in the full method name.
Other methods in the same protocol are: application:shouldSaveApplicationState: and application:willChangeStatusBarFrame:.Problems start cropping when you try to translate these methods to a Delphi representation.
function application(application: UIApplication;
didFinishLaunchingWithOptions: NSDictionary): Boolean; cdecl; overload;
function application(application: UIApplication;
shouldSaveApplicationState: NSCoder): Boolean; cdecl; overload;
procedure application(application: UIApplication;
willChangeStatusBarFrame: CGRect); cdecl; overload;
As you see all methods share the same name
so it requires overloading. It’s ok for the moment since all the methods have
different signatures.
Let’s add the application:shouldRestoreApplicationState:
method to the previous sample:.
function application(application: UIApplication;
shouldRestoreApplicationState: NSCoder): Boolean; cdecl; overload;
When we translate this additional method
compiler will reject this overload since we have two methods with the same
signature.
To solve this problem a new attribute can
be attached to interface methods [MethodName()] which helps you to map an arbitrarily named Delphi method to a
specifically named Objective-C method.So previous method could be renamed as:
[MethodName(‘application:shouldRestoreApplicationState:’)]
function applicationShouldRestore(application:
UIApplication; shouldRestoreApplicationState: NSCoder): Boolean; cdecl;
2.5.- Objective-C properties
Delphi interfaces that represent
Objective-C objects don’t wrap Opjective-C object properties so you’ll have to
use the getter and setter functions.
For example with UIView to access to the frame property you’ll write AFrame := AView.frame and to modify the frame property you’ll write AView.setFrame(AFrame).You can read the formal documentation on property accessor naming on Apple site’s here: onApple site’s here.
2.6.- Delphi local implementations of Objective-C objects
Local implementations of Objective-C objects
are needed to implement Objective-C protocols or interfaces or when you need to
subclass an existing Objective-C class.
The base class for local implementations of
Objective-C objects is TOLocal which
is defined in Macapi.ObjectiveC.To implement an Objective-C interface or protocol all you need to do is to declare the new interface and derive a new class from TOLocal which implements that interface.
This code sample shows you how to do it:
type
VideoCaptureDelegate =
interface(IObjectiveC)
['{95C26C24-9DB3-441A-A60D-A20E96BEF584}']
procedure
observeValueForKeyPath(keyPath: NSString; ofObject: Pointer; change:
NSDictionary; context: Pointer); cdecl;
…
end;
TVideoCaptureDelegate =
class(TOCLocal, VideoCaptureDelegate)
private
[Weak] FMain: TFMain;
FFlashView: UIView;
public
constructor Create(Main:
TFMain);
procedure
observeValueForKeyPath(keyPath: NSString; ofObject: Pointer; change:
NSDictionary; context: Pointer); cdecl;
…
end;
If we need to subclass any existing Objective-C class we have to override the GetObjectiveCClass method.
Take a look to the TFMXViewController class defined in FMX.Platform.iOS. It is a sample of subclassing an existing Objective-C class.
type
FMXViewController = interface(UIViewController)
['{FB1283E6-B1AB-419F-B331-160096B10C62}']
…
function
shouldAutorotate: Boolean; cdecl;
…
end;
TFMXViewController = class(TOCLocal)
protected
function
GetObjectiveCClass: PTypeInfo; override;
public
constructor Create;
…
function
shouldAutorotate: Boolean; cdecl;
…
end;
function TFMXViewController.GetObjectiveCClass: PTypeInfo;
begin
Result :=
TypeInfo(FMXViewController);
end;
If as a result of calling the initialization method the object id is changed then it updates the id calling the UpdateObjectId method.
constructor TFMXViewController.Create;
var
V: Pointer;
begin
inherited;
V :=
UIViewController(Super).initWithNibName(nil, nil);
if
GetObjectID <> V then
UpdateObjectID(V);
end;
2.7.- Direct call to Objective-C methods
It is possible to call to Objective-C
methods without the definition of interface records.
The procedure cbjc_msgSend defined in unit Macapi.ObjCRuntime can be used for this purpose.
The arguments for this procedure are used as follow:
The procedure cbjc_msgSend defined in unit Macapi.ObjCRuntime can be used for this purpose.
The arguments for this procedure are used as follow:
- theReceiver: This argument takes an Objective-C object id that represents the object on which the method is going to be executed.
- theSelector: This method takes a raw pointer to the selector for the method that is going to be executed. To get a selector for a method you can call the sel_getUid function that takes a string with the Objective-C method name.
- Optional arguments: You may add as many arguments as the method needs.
objc_msgSend((FStillImageOutput as ILocalObject).GetObjectID,
sel_getUid('addObserver:forKeyPath:options:context:'),
FVideoCaptureDelegate.GetObjectID,
(NSSTR('capturingStillImage') as ILocalObject).GetObjectID,
NSKeyValueObservingOptionNew,
(FAVCaptureStillImageIsCapturingStillImageContext as
ILocalObject).GetObjectID);
objc_msgSend(objc_getClass('CATransaction'), sel_getUid('begin'));
2.8.- ARC and Objective-C wrapped objects
Delphi NextGen compiler implements automatic
reference counting (ARC) for all Delphi objects as described in this DrDobbs article by Embo's JT and Marco Cantù.
The compiler will manage the logic to TObject’s
__ObjAddRef and __ObjRelease for you.Objective-C code uses the same logic to call retain and release.
Unfortunately there is no ARC for the Objective-C objects represented by the import wrapper class and the interfaces discussed above. When dealing with Objective-C objects you’ll have to call retain and release yourself at the correct points.
Allocating a new Objective-C object will initialize its reference count to 1 and calling release will drop it to 0 thus destroying it.
Objective-C methods that return new objects add those objects to the autorelease pool. The runtime call release method to any object contained in the autorelease pool when control returns from the main loop.
If you have an interface that represents an Objective-C object created with autorelease and there are no more Objective-C references to that object you must call retain to avoid the object to be automatically released when control returns from the main loop.
2.9.- Dealing with NSStrings
Objective-C uses NSString objects where Delphi uses strings. If you need to pass a string to an iOS API that expects an NSString you can use the NSStr function which is defined in iOSapi.Foundation.Also you can convert an NSString to a Delphi string using the NSStrToStr function which is defined in the FMX.Helpers.iOS unit. This function turns the NSString to UTF8 before turning it back to a Delphi string. Thus any characters outside UTF8-space get mangled. You might benefit from looking at Chris Rollinston’s approach to this problem.
Note: NStrings created with the NSStr function use the Objective-C stringWithCharacters class method of NSString. Strings created with this method are autoreleased. Be sure to call retain method for the returned NSString if there are no more Objective-C references to the string than your Delphi interface and that string is going to persist after you return to the main loop.
2.10.- Dealing with C strings
C Strings are null terminated strings.
System unit defines the MarshaledAString type that helps you to interface with C code that uses the (char *) type.
You can directly cast a String to a MarshaledAString. To convert a returned MarshaledAString to a String you can use the UTF8ToString function.3.- Grand central dispatch
Grand central dispatch (GCD) is a technology developed by Apple to deal with multithreading code. It is an implementation of task parallelism based on the thread pool pattern.SquareCam uses GCD to process video information in separate threads to smooth the application behavior.
We have to extend Delphi basic implementation of GCD in unit Macapi.Dispatch to work with dispatch_async and dispatch_sync methods.
Those two methods take a dispatch_queue as the first argument and an Objective-C code block as the second argument. That code block will be called in the thread specified by the dispatch_queue argument.
Currently there is no easy way to get callback to the Obkective-C code block parameter. We have a workaround by using the dispatch_async_f and dispatch_sync_f methods.
The process uses Delphi anonymous procedures in a somewhat tricky way:
type
dispatch_work_t = reference
to procedure;
dispatch_function_t =
procedure(context: Pointer); cdecl;
procedure dispatch_async_f(queue: dispatch_queue_t; context:
Pointer; work: dispatch_function_t); cdecl; external libdispatch name _PU +
'dispatch_async_f';
procedure dispatch_async(queue: dispatch_queue_t; work:
dispatch_work_t);
procedure DispatchCallback(context: Pointer); cdecl;
var
CallbackProc: dispatch_work_t
absolute context;
begin
try
CallbackProc;
finally
IInterface(context)._Release;
end;
end;
procedure dispatch_async(queue: dispatch_queue_t; work:
dispatch_work_t);
var
callback: Pointer absolute
work;
begin
IInterface(callback)._AddRef;
dispatch_async_f(queue,
callback, DispatchCallback);
end;
Then we can use the dispatch_async procedure in a similar way as Objective-C does:
begin
…
dispatch_async(dispatch_get_main_queue,
procedure
begin
DrawFaceBoxesForFeatures(Features,
Clap, CurDeviceOrientation);
end);
…
end;
4.- Application source code
Now it’s time to go to the code and navigate through it to see how the concepts we’ve been reviewing can be applied to real applications.You can download the code for this project here.