sábado, 25 de enero de 2014

The quest to migrate iOS SquareCam App to Firemonkey


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.
You can find a sample of this in the DataSnap.DSIntf unit:

{$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.
TOCGenericImport class has equivalences to those initialization methods:
  • Alloc: for the Objective-C alloc.
  • Create: For the Objective-C new (alloc and init).
The process of creating an Objective-C object with a custom initialization method is described in the following code sample:

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;

If you need to access the object Id that is represented by an interface you can use the following code:

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;

When we invoke the constructor of TVideoCaptureDelegate an Objective-C object descendant of NSObject is created that implements the interface VideoCaptureDelegate. We can use that object to interact with any of the Objective-C existing objects.
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;

GetObjectiveCClass function returns the interface for the Objective-C object to be created. It will be used to define the class name of the Objective-C class to be registered.

function TFMXViewController.GetObjectiveCClass: PTypeInfo;
begin
  Result := TypeInfo(FMXViewController);
end;

Constructor for this class calls the inherited constructor to allocate the Objective-C object. Then it calls the superclass custom initialization method initWithNibName.
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:
  • 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);

You can call to a class method using the following code:

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);

Anonymous procedures are implemented in Delphi with interfaces. We call the dispatch_async_f procedure passing the interface that represents the anonymous procedure as the context parameter:

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.

5.- Application screenshot

6.- Special thanks

Special thanks go to Brian Long. His article Delphifor iOS – some notes saved me a lot of time in this project.

7 comentarios:

  1. Hello,

    Your article is very helpful for those who want to take the leap into iOS development using Delphi & Firemonkey. You have successfuly used the ALAssetsLibrary framework for writing the images to the photo album but have you ever used its "enumerateGroupsWithTypes" method? I get access violations when trying to access it (I am using Delphi XE6 Update 1).

    Thanks,
    Kostas

    ResponderEliminar
    Respuestas
    1. Hello Kostas, as I stated in my article, here is no easy way to call an Objective C block from Delphi. There are some solutions: You can write an Objective C dylib with a C procedure that calls enumerateGroupsWithTypes method but takes pointer to C callback functions as parameters. Second, you can try the solution provided at http://stackoverflow.com/questions/23130639/calling-objective-c-code-block-from-delphi

      Eliminar
  2. Hi, there,
    As your sample code, there is a "GUID" for the interface, e.g., sample code in 2.6, ['{95C26C24-9DB3-441A-A60D-A20E96BEF584}'] for VideoCaptureDelegate.

    How can we determine the GUID for each interface? Is that defined by Xcode? or defined by Delphi? or other else?

    I am trying to import 3rd party framework for iOS into Delphi, and I don't understand how to get this id to import the interface provided by Framework.

    Best Regard,
    Dennies.

    ResponderEliminar
    Respuestas
    1. This GUID is created to help Delphi to identify the interface. See http://docwiki.embarcadero.com/RADStudio/Seattle/en/Object_Interfaces for more information. When you are creating new interfaces you may synthesize the GUID by pressing Ctrl+Shift+G keys.

      Eliminar
  3. Thanks a lot but can not compile your code from github.com/ChristenBlom/SquareCamFmx in Delphi Rio 10.3.3

    I have an error with "objc_msgSend" in Macapi.ObjCRuntime: "Too many actual parameters" :(

    Could you please help with it?

    Thanks!

    ResponderEliminar
    Respuestas
    1. With newer versions of Delphi you should define the function:

      function objc_msgSendP4(theReceiver: Pointer; theSelector: Pointer; P1,P2,P3,P4: Pointer): Pointer; cdecl; overload;
      external libobjc name _PU + 'objc_msgSend';

      And then you can use it as follows:

      objc_msgSendP4((FStillImageOutput as ILocalObject).GetObjectID,
      sel_getUid('addObserver:forKeyPath:options:context:'),
      FVideoCaptureDelegate.GetObjectID,
      (StrToNSStr('capturingStillImage') as ILocalObject).GetObjectID,
      Pointer(NSKeyValueObservingOptionNew),
      (FAVCaptureStillImageIsCapturingStillImageContext as ILocalObject).GetObjectID);

      Eliminar