Delphi für iOS und externe Bibliotheken (“.a-Libraries”)

Wenn man sich mit der iOS-Programmierung beschäftigt, wird man schnell lernen, dass das dynamische Einbinden von externen Bibliotheken vom System nicht vorgesehen ist. Gelegentlich findet man sogar die Aussage, man könne gar keine Bibliotheken verwenden – das ist jedoch falsch. Der entscheidende Punkt ist, dass jeglicher Code einer iOS-App statisch eingebunden sein muss.

Hintergrund sind primär Sicherheits-Aspekte: Würde man einer App gestatten, dynamisch Code einzubinden, könnte sie diesen vorher z.B. aus dem Internet nachladen. Damit wäre das App-Ökosystem auf einem iOS-Gerät völlig unkontrolliert und würde jegliche Sicherheits-Maßnahmen ad absurdum führen.

Das Einbinden externer Bibliotheken geschieht in drei (vier) Schritten, die hier anhand eines einfachen Beispiel-Projektes gezeigt werden.

Schritt #1: Die Bestandsaufnahme

Es sind verschiedene Dateien und Dateitypen, die eine Bedeutung erlangen werden. Grund genug, einen Moment an Zeit zu investieren, um sich zu vergegenwärtigen, womit man es eigentlich zutun hat. Ausgangspunkt sind ObjectiveC-Dateien, teils kompiliert, teils im Quellcode. Es gibt i.A. wenigstens zwei Dateien:

  • Die kompilierte Bibliothek selbst. Das Kompilat kann Binärcode für verschiedene Architekturen enthalten wie z.B. Intel x86/x64 oder ARMv6, ARMv7 etc. Klassischerweise haben die statischen Bibliotheken die Dateiendung “.a”. (Solange man sie statisch linkt, lassen sich auch dynamische Bibliotheken (“.dylib”) in eine iOS-App integrieren.)
  • Die dazu passende Header-Datei, welche im Allgemeinen den gleichen Namen trägt wie die Bibliothek, jedoch mit dem Suffix “.h”. In dieser Header-Datei sind die Typ-Deklarationen (Konstanten, Records, Klassen, etc.) und Schnittstellen (Funktionen etc.) der Bibliothek enthalten.

Ausgangspunkt für dieses Beispiel ist eine einfache ObjectiveC-Bibliothek, welche eine Klasse “MyLibrary” enthält sowie eine Funktion “Moep”. Der Header für diese Bibliothek sieht wie folgt aus:

1
2
3
4
5
6
7
8
9
10
11
//  MyLibrary.h
#import <Foundation/Foundation.h>;
 
// a class called "MyLibrary" containing one instance-method "Increment" that
// takes a single parameter of type integer and returns an integer
@interface MyLibrary : NSObject
-(int)Increment:(int)a;
@end
 
// a simple function taking no parameters and returning an integer
int Moep();

// a class called "MyLibrary" containing one instance-method "Increment" that
// takes a single parameter of type integer and returns an integer
@interface MyLibrary : NSObject
-(int)Increment:(int)a;
@end

// a simple function taking no parameters and returning an integer
int Moep();


Zugegebenermaßen ein simples Beispiel, das jedoch langen sollte, um ein Verständnis für die Thematik zu erlangen.

Schritt #2: Übersetzen des Bibliothek-Headers

Der Bibliothek-Header, die .h-Datei, liegt im ObjectiveC-Quellcode vor und muss zwangsläufig in die Delphi-Sprache übersetzt werden.

Vorab die gute Nachricht: Man muss nur die Teile des Headers übersetzen, die man direkt oder indirekt verwendet. Es besteht keine Notwendigkeit, pauschal den gesamten Header zu übersetzen.

Dieser Arbeitsschritt artet gern mal in “Fummelkram” aus und erfordert mal mehr, mal weniger Kenntnisse in ObjectiveC. Die Klassiker hierbei sind Konstanten, Records und Klassen mit Feldern, Eigenschaften sowie Instanz- und ggf. auch Klassenmethoden. Ab und an vielleicht einfache Funktionsaufrufe und das alles mit oder ohne Parametern. Das klingt etwas gräßlicher als es ist, denn die syntaktischen Elemente und Datentypen wiederholen sich natürlich und man bekommt schnell einen Blick dafür, wie die Dinge zu übersetzen sind. Zumal es bei der Übersetzung auch keinerlei “künstlerische Freiheit” gibt. Es ist elementar wichtig, hier präzise zu arbeiten: Ist der Header falsch übersetzt, sind Fehlfunktionen beim Aufruf der Bibliothek quasi unausweichlich – “vorprogrammiert” gewissermaßen.

Es gibt Werkzeuge, die bei der Konvertierung helfen können. Bei den Developer-Experts befindet sich ein derartiges Werkzeug aktuell in Produktion. Eines haben all diese Werkzeuge jedoch gemein: Sie können lediglich unterstützen – so gut wie niemals werden sie die vollständige Arbeit übernehmen und kompilierfähige Resultate abliefern können.

Möchte man Klassen aus ObjectiveC in Delphi importieren, so muss man wissen, wie diese für Delphi aufbereitet werden müssen. Pauschal lässt sich sagen, dass aus einer einzigen ObjectiveC-Klasse in Delphi grundsätzlich drei Code-Elemente werden:

  • Ein Interface, welches die Instanz-Methoden und Eigenschaften der zu importierenden Klasse enthält – abgeleitet von “NSObject”
  • Ein Interface, welches die Klassen-Methoden der zu importierenden Klasse enthält – abgeleitet von “NSObjectClass”
  • Eine Klasse, welche die beiden Interfaces zusammenführt – abgeleitet von “TOCGenericImport”

Dieser “Dreisatz” an Interfaces und Klassen wird bei jedem Import gegenwärtig sein. Die konkrete Übersetzung des o.g. Beispiel-Header sieht in Delphi wie folgt aus:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const
  LIB_MYLIBRARY = 'libMyLibrary.a';
 
type
  { MyLibrary / TMyLibrary }
 
  // non-static (instance) methods of "MyLibrary"
  MyLibrary = interface(NSObject)
    function Increment( a : integer ) : integer; cdecl;
  end;
 
  // static (class) methods of "MyLibrary"
  MyLibraryClass = interface(NSObjectClass)
  end;
 
  TMyLibrary = class(TOCGenericImport<MyLibraryClass, MyLibrary>)
  end;
 
function Moep : integer; cdecl; external LIB_MYLIBRARY name 'Moep';

type
{ MyLibrary / TMyLibrary }

// non-static (instance) methods of "MyLibrary"
MyLibrary = interface(NSObject)
function Increment( a : integer ) : integer; cdecl;
end;

// static (class) methods of "MyLibrary"
MyLibraryClass = interface(NSObjectClass)
end;

TMyLibrary = class(TOCGenericImport<MyLibraryClass, MyLibrary>)
end;

function Moep : integer; cdecl; external LIB_MYLIBRARY name ‘Moep’;


Die verwendeten Klassen “NSObject”, “NSObjectClass” und “TOCGenericImport” werden von Delphi zu Verfügung gestellt und sorgen für den eigentlichen Import, der im Hintergrund abläuft und um den man sich – glücklicherweise – nicht weiter kümmern muss. Beim Betrachten der Klasse und der Interfaces fällt auf, dass diese scheinbar allein für sich stehen und keinerlei Referenzen auf die Bibliothek aufweisen. Die Klassen und Methoden werden unter iOS zur Laufzeit über ihren Namen aufgefunden und angesprochen. Der korrekte Name ist also einer der Schlüssel für das Funktionieren der importierten Klassen und Funktionen.

Bei Methoden und Funktionen ist grundsätzlich darauf zu achten, dass man die sog. Aufruf-Konvention (“Calling-Convention”) angibt – in diesem Fall “cdecl”. Diese Konvention steuert, wie die Parameter und Rückgabewerte bei Funktionsaufrufen behandelt werden.

Schritt #3: Statisches Referenzieren der Bibliothek

Eine “App” ist ein sog. Package. Vom Grundgedanken her vergleichbar beispielsweise mit einem ZIP-Archiv, welches selbst auch nur ein Container ist und eine Dateistruktur beinhaltet. Neben der ausführbaren Programmdatei selbst sind z.B. die Launch-Images (“Splash-Screens”), verschiedene Icons und ggf. weitere Dateien zu finden. Es reicht ausdrücklich nicht, wenn die zu importierende Bibliothek als Datei im Rahmen dieses App-Packages ausgeliefert wird: Nein, die Bibliothek muss von Linker binär in die ausführbare Programmdatei eingebettet werden (unter Windows wäre dies die .EXE-Datei, in die die Bibliothek eingebettet werden muss).

Dieses Einbetten übernimmt der Linker, wenn man ihn denn entsprechend anweist. Am einfachsten geht dies durch eine statische Deklaration, die einen Typ oder eine Funktion aus der Bibliothek verwendet. Auf diese Weise wird schon zur Designzeit und damit natürlich auch zur Kompilierzeit eine direkte Referenz in die Bibliothek hinein erstellt und das lässt dem Linker dann keine andere Wahl, als diese Bibliothek zu integrieren. Dies hat die für App keinen unmittelbaren Mehrwert sondern dient allein dazu, den Rahmenbedingungen vom iOS gerecht zu werden. Vor diesem Hintergrund wird so eine statische Referenz in die Bibliothek auch gern “Fake-Loader” genannt. “Fake” deswegen, weil man diese Referenz im Allgemeinen nicht aufrufen wird sondern lediglich aus den o.g. Gründen erstellt hat.

Der Linker scheint die Bibliothek nur für Kompilate, die direkt auf das Gerät gehen, einbinden zu können. Kompiliert man die App für den Simulator, so ist die Bibliothek nicht enthalten. Aus diesem Grund ist der Fake-Loader mit IFDEFs umschlossen.

Im folgenden wird eine Funktion erstellt, welche einen der in der Bibliothek enthaltenen Typ – hier die Klasse “TMyLibrary” zurückliefert.

1
2
3
4
5
6
7
8
9
10
11
12
13
implementation
{$IF DEFINED(CPUARM)}
  function FAKE_LOADER : TMyLibrary; cdecl; external LIB_MYLIBRARY name 'OBJC_CLASS_$_MyLibrary';
{$IFEND}
 
initialization
{$IF DEFINED(CPUARM)}
  /// although this code is NEVER executed, it ensures a strong reference
  ///  to the library - the linker is gonna LOVE (and consume) it!
  if FALSE then
    FAKE_LOADER;
{$IFEND}
end.

initialization
{$IF DEFINED(CPUARM)}
/// although this code is NEVER executed, it ensures a strong reference
/// to the library – the linker is gonna LOVE (and consume) it!
if FALSE then
FAKE_LOADER;
{$IFEND}
end.


Der Linker kann an dieser Stelle nicht wissen, dass es diese Funktion in der Bibliothek gar nicht gibt – ihm bleibt nichts anderes übrig, als diese Referenz zu berücksichtigen und als Konsequenz die Bibliothek statisch einzubinden. Diese Form des Fake-Loaders darf unter keinen Umständen aufgerufen werden, da dies logischerweise zu einer Schutzverletzung führen würde.

Wichtiger Hinweis: Dieser Weg, externe Bibliotheken einzubinden, funktioniert ausschließlich auf dem Gerät selbst, nicht im Simulator.

Schritt #4: Verwenden der Bibliothek

Ab hier wird es wieder einfach, denn alles, was nun folgt, spielt sich rein in der gewohnten Syntax der Delphi-Sprache ab. Die Verwendung der in der Bibliothek enthaltenen Funktion “Moep” erweist sich nun als geradezu trivial:

1
2
3
4
5
6
procedure TfrmMain.DoUseFunction;
var x : integer;
begin
  x:= Moep;
  ShowMessage( 'calling "Moep" returned ' + x.ToString );
end;

Der Aufruf der Klasse “TMyLibrary” ist nicht nennenswert schwieriger. Man muss nur gut aufpassen, wann man die Klasse und wann eines der Interfaces verwendet:

1
2
3
4
5
6
7
8
9
procedure TfrmMain.DoUseClass;
var LFoo : MyLibrary;  // <-- use interface here
       x : integer;
begin
  LFoo:= TMyLibrary.Create;  // <-- use class here
  x:= LFoo.Increment( 41 );
 
  ShowMessage( 'calling "Increment" returned ' + x.ToString );
end;

ShowMessage( ‘calling "Increment" returned ‘ + x.ToString );
end;


Damit sind alle Bausteine zusammen, um eine externe ObjectiveC-Bibliothek in einer mit Delphi XE4 geschriebenen iOS-App zu verwenden. Für Disussionen und Fragen zu diesem Beitrag sei auf das Forum “Delphi-PRAXiS” verweisen.

Beispielprojekte herunterladen

0
Daniel Wolf

About the Author:

Daniel Wolf ist als Software-Architekt im Miniatur Wunderland Hamburg tätig, war zuvor viele Jahre als technischer Berater tätig und spricht regelmäßig auf Konferenzen. Er betreibt das Forum "Delphi-PRAXiS". » Mehr Details zur Person
  Verwandte Blog-Einträge