Lua und Swift – Teil 1

  • Komplexe Anwendungen werden durch Skriptsprachen leichter automatisierbar und konfigurierbar
  • Lua lässt sich besonders einfach und elegant in bestehende Projekte integrieren

Der Anfang einer wunderbaren Freundschaft

Als Softwareentwickler steht man oft vor der Herausforderung, ein Projekt mit hoher Komplexität möglichst leicht erweiterbar, wiederverwendbar und wartungsfreundlich zu halten.

Ebenso kommt es häufig vor, dass man Teile eines komplexen Projektes für einen Anwender ohne Zugriff auf den Quellcode (beziehungsweise Fachwissen über die Funktionsweise desselben) automatisierbar und konfigurierbar machen möchte.

Ein möglicher und beliebter Ansatz bei beiden Problemstellungen ist es, den komplexen Teil eines Projektes durch Verwendung einer Skriptsprache auf einem höheren Abstraktionsniveau zugänglich zu machen.

Die Liste der für solch ein Vorhaben in Frage kommenden Skriptsprachen ist lang: Javascript, Lua, Perl, Python, PHP und Ruby bieten sich an, nur um mal die bekannteren zu nennen.

Wie der Titel des Postings vermuten lässt, möchte ich in diesem und den folgenden Postings ein bißchen etwas darüber erzählen, wie man Lua am besten in ein Swift-Projekt integriert. Dabei werden wir mit einem sehr einfachen Beispiel anfangen, dieses ausbauen und zum Schluss bei einer mehr oder weniger sinnvollen, vollwertigen App mit Lua-Integration ankommen.

Besonderes Augenmerk werden wir auf die interne Funktionsweise von Lua legen, denn schließlich haben wir mit Lua noch Großes vor, und das Ziel dieser Reihe soll sein, dass Lua zum Schluss nicht mehr nur eine kleine, schnelle und geheimnisvolle Black Box ist, die Lua-Code ausführt.

Der ein oder andere Leser wird sich spätestens zu diesem Zeitpunkt eventuell die folgende Frage stellen:

Was ist dieses Lua überhaupt, und warum möchte man es verwenden?

Lua wurde 1993 an der Päpstlichen Katholischen Universität von Rio de Janeiro in Brasilien in der dortigen Abteilung für Computergrafik, der „Tecgraf“ entwickelt. Von 1977 bis 1992 herrschten in Brasilien strenge Handelsbarrieren für Hard- und Software. Diese Barrieren verhinderten, dass irgendwelche Technologie aus dem Ausland eingekauft wurde – was wiederum dazu führte, dass bei der Tecgraf über lange Jahre hinweg grundlegende Tools selbst implementiert wurden.

Die Tecgraf hatte damals mit der Petrobas, einem multinationalen Konzern aus der Ölundustrie, einen Partner, für den in der Vergangenheit bereits einfache Programmiersprachen zu Konfiguration und Abfrage industrieller Anwendungen entwickelt worden waren. Mit der Zeit wurde der Wunsch nach einer universell einsetzbaren, einfach zu erlernenden Skriptsprache lauter und lauter.

Javascript existierte noch lange nicht, Python steckte gerade erst in den Kinderschuhen, und Tcl und Perl waren auch in den frühen 90ern schon schrecklich. In der damals vorherrschenden Do-It-Yourself-Atmosphäre bei der Tecgraf lag der Gedanke nahe, eine einfach zu erlernende Skriptsprache selbst zu erschaffen.

Durch diese vom Rest der Welt abgetrennte Entwicklung verlief die Evolution von Lua wesentlich ruhiger und überlegter als beispielsweise die von JavaScript und Python, und das ist meiner Ansicht nach auch heute noch deutlich zu merken – es gab nie den externen Druck, Lua ganz schnell mit ganz fürchterlich vielen tollen Features auszustatten, und so besann sich das Lua-Team auf die konsequente Fortführung und Pflege der fundamentalen Designkonzepte von Lua.

Natürlich ist die Wahl einer Programmiersprache auch Geschmacksache, doch es gibt außer meiner persönlichen Präferenz natürlich noch einige ganz rationale Gründe, die für die Verwendung von Lua sprechen:

  • Es ist klein. Ein Lua-Interpreter mit allen Standard-Libraries braucht 247 KB. Gemeint sind Kilobyte. Zum Vergleich: Ein einfaches „Hello World“ in Swift ist mit 6.4 MB knapp 24 mal so groß (übrigens, ein „Hello World“ in Objective-C ist 24 KB groß, nur mal so vollkommen wertfrei in den Raum gestellt).
  • Es ist simpel und konsequent. Die zwei grundlegenden Konzepte von Lua sind Tabellen und Meta-Mechanismen. Lua ist an sich nicht objektorientiert, durch die erwähnten Meta-Mechanismen lassen sich jedoch ohne Probleme Konzepte wie Klassen und Vererbung implementieren.
  • Es ist portabel und stellt sehr geringe Anforderungen an das Betriebssystem. Lua läuft unter Windows, Linux, macOS und iOS, es läuft aber auch ohne Probleme auf exotischen Plattformen wie RiscOS, dem Lego Mindstorms NXT und Rabbit-Microcontrollern.
  • Es ist frei. Lua steht unter der MIT-Lizenz. Es kann und darf für alle möglichen Zwecke, kommerziell und nicht-kommerziell verwendet werden, und, der m.E. gewichtigste Grund:
  • Es lässt sich sehr leicht in andere Programmiersprachen integrieren. Lua wurde quasi dafür geschaffen, in ein C/ObjectiveC/C++-Projekt eingebunden zu werden. Mit Swift ist ist es ein kleines bißchen komplizierter, aber für Swift-Verhältnisse immer noch einfach genug.

Viele bekannte und erfolgreiche Anwendungen und Spiele verwenden Lua. Die populärsten Beispiele sind sicherlich World of Warcraft und Angry Birds. Aber auch das Frontend von Adobe Photoshop Lightroom ist in Lua geschrieben, außerdem dient Lua als Skriptsprache für den VideoLanClient (VLC), für die 3D-Modelling-Software Strata3D , das Netzwerk-Analysetool Wireshark… und viele, viele andere Anwendungen mehr. 

Ein „Minimum Viable Hello World“ mit Swift und Lua

Wir beginnen unser Lua-Swift-Abenteuer mit einer ausgesprochen populären, wichtigen und vielseitigen Anwendung.

Richtig, gemeint ist „Hello World“. Unser erstes Ziel soll es sein, aus einem Swift-Programm einen Lua-Interpreter zu starten, der ein Skript ausführt, welches „Hello, World!“ auf der Konsole ausgibt.

Und wenn das jetzt etwas kompliziert klingt, dann sei als Trost gesagt, dass wir die Implementierung (zumindest vorläufig) so einfach wie möglich halten. Quasi ein Minimum Viable Hello World, um die Product Owner dieser Welt in Ekstase zu versetzen.

Um Lua in ein Swift-Projekt zu integrieren, wird zunächst einmal der Lua-Quellcode benötigt. Dieser befindet sich unter https://www.lua.org/download.html

Als nächstes wird das Xcode-Projekt angelegt. Unser Projekt soll ein simples Tool für die Kommandozeile werden, also wählen wir in Xcode unter „File -> New -> Project…“ als Plattform „macOS“ und als Projekttyp „Command Line Tool“ aus: 

Als Sprache wird Swift ausgewählt:

Nun stellt sich die Aufgabe, den Lua-Quellcode in Xcode zum Compilieren zu bekommen. Wie eingangs schon erwähnt, ist Lua äußerst genügsam in seinen Anforderungen an das Betriebssystem. Es reicht in unserem Fall also tatsächlich, den „src“-Ordner aus dem Lua-Archiv in den Projektnavigator von Xcode zu ziehen

Xcode fragt daraufhin nach, ob ein Projekt mit externem Build-System erzeugt werden soll. Dies geschieht deshalb, weil der „src“-Ordner ein Makefile enthält.

Dieses Makefile ist dazu da, Lua als Standalone-Anwendung zu bauen. Da wir dies nicht wollen (und uns stattdessen der Freude hingeben werden, Lua vollumfänglich in unser Swift-Projekt zu integrieren), lehnen wir dankend ab:

Als nächstes kommt der Standard-Dialog, den Xcode beim Hinzufügen von Dateien zu einem Projekt anzeigt.

An dieser Stelle ist es insbesondere wichtig, die Dateien zu unserem „LuaSwiftHelloWorld“-Target hinzuzufügen: 

Jetzt müssen wir nur noch ein klein wenig aufräumen: Der „src“-Ordner von Lua enthält die Files lua.c und luac.c, wobei es sich um den Standalone-Interpreter und -Compiler von Lua handelt. Diese brauchen wir beide nicht – mehr noch, sie zu behalten wäre schädlich, denn sie enthalten beide ein „main“-Symbol, welches nachher unseren Compiler durcheinanderbringt. 

Also weg damit – und wo wir schon dabei sind, kann auch noch gleich das Makefile gelöscht werden:

Der Bridging Header

Wer ungeduldig und/oder neugierig ist, der kann jetzt schon mal „Cmd+B“ drücken und zuschauen, wie Lua gebaut wird. Der Compiler wirft bei dieser Gelegenheit ein paar Warnings (zumindest tut er das bei Lua 5.3.5), aber das soll uns jetzt nicht weiter interessieren.

Um Lua nun wirklich zusammen mit Swift verwenden zu können, brauchen wir noch einen Bridging Header. Im Bridging Header wird festgelegt, welche Symbole aus C (oder Objective-C oder C++) auf der Swift-Seite zugänglich gemacht werden sollen. 

Normalerweise wird der Bridging Header von Xcode automatisch erzeugt, wenn zu einem Swift-Projekt Objective-C-Dateien hinzugefügt werden. Leider gilt dies nicht für ordinären C-Quellcode, hier müssen wir selbst Hand anlegen.

Per Konvention wird der Bridging Header nach dem Muster „Projektname-Bridging-Header.h“ benannt. Wir erzeugen also per Menüauswahl „New -> File…“ ein neues File, wählen „Header File“ als Filetyp und nennen das neue Header-File „LuaSwiftHelloWorld-Bridging-Header.h“ (oder anders, je nachdem wie wir das Projekt genannt haben).

Im Bridging Header selbst importieren wir nun diejenigen Header-Files aus den Lua-Quellen, deren Symbole wir auf der Swift-Seite verwenden wollen. Es sind dies lua.hlauxlib.h und lualib.h 

Der Bridging Header sieht also insgesamt folgendermaßen aus:

//
//  LuaSwiftHelloWorld-Bridging-Header.h
//  LuaSwiftHelloWorld
//
//  Created by Stephan Kleinert on 21.01.19.
//  Copyright © 2019 TheAppGuys. All rights reserved.
//

#ifndef LuaSwiftHelloWorld_Bridging_Header_h
#define LuaSwiftHelloWorld_Bridging_Header_h

#import "lua.h"
#import "lauxlib.h"
#import "lualib.h"

#endif /* LuaSwiftHelloWorld_Bridging_Header_h*/

 

Nun müssen wir dem Compiler nur noch mitteilen, wo er den Bridging-Header finden kann. Dies geschieht in den „Build Settings“. Die gesuchte Konfiguration befindet sich unter „Swift Compiler – General“, aber am einfachsten ist es, im Suchfeld nach „Bridging“ zu suchen und dann den Namen der Datei in der entsprechenden Zeile einzutragen:

Und das war es auch schon – der Verwendung von Lua in unserem Swift-Programmcode steht nun zumindest theoretisch nichts mehr im Wege. 

Und weil alles bis hierher schon lange genug gedauert hat, hier nun endlich mal ein bisschen Code:

Hello World (Sprint 1 ;-))


//
//  main.swift
//  LuaSwiftHelloWorld
//
//  Created by Stephan Kleinert on 22.01.19.
//  Copyright © 2019 TheAppGuys. All rights reserved.
//

import Foundation

let myLuaState = luaL_newstate()
luaL_openlibs(myLuaState)

let myScript = "print \"Hello, World!\"\n"

luaL_loadbufferx(myLuaState,
                 myScript,
                 myScript.count,
                 nil, nil)

lua_callk(myLuaState, 0, 0, 0, nil)

Dies ist, soweit ich das beurteilen kann, die kürzest mögliche Version unseres Ansinnens, von Lua-Seite aus ein „Hello World“ auf die Konsole eines Swift-Tools auszugeben. Leider ist der Code auch ziemlich fahrlässig, und wir werden ihn definitiv nicht so lassen. Aber er funktioniert, und er ist in seiner Kürze prima dazu geeignet, ein paar grundlegende Konzepte der Lua-Integration in andere Programmiersprachen zu erklären.

lua_ vs. luaL_

Dem aufmerksamen Leser wird bei obigem Code eventuell auffallen, dass Lua-Funktionen einmal mit dem lua_– und einmal mit dem luaL_-Prefix ausgestattet sind. Was hat es damit auf sich?

Funktionen mit dem lua_-Prefix richten sich an die C-API für Lua. Hier geht es um all jene zentralen Funktionen, die es erlauben, den Lua-Stack zu manipulieren, mit Lua-Datentypen zu arbeiten und die Ausführung von Lua-Code zu beeinflussen.

Funktionen mit dem luaL_-Prefix sind in der sogenannten Auxiliary Library (lauxlib.h) definiert. Die Auxiliary Library stellt diverse High-Level-Funktionen zur Verfügung, um Lua und C miteinander zu verbinden. Im Prinzip liesse sich alles, was die Auxiliary Library leistet, auch mit der zentralen C-API bewerkstelligen, aber es wäre wesentlich mühsamer.

Der Lua State

Lua ist vollkommen re-entrant und verwendet keinerlei globale Variablen. Alle benötigten Daten zum Interpretieren eines Lua-Skriptes werden in einer dynamischen Struktur, dem sogenannten Lua State gespeichert. Vereinfacht ausgedrückt referenziert ein Lua State einen eigenen und unabhängigen Lua-Interpreter. Es können im Prinzip beliebig viele Lua States erzeugt werden, welche wiederum in beliebig vielen Threads parallel laufen dürfen.

Für unser kleines „Hello, World“ (sowie für die meisten anderen Anwendungsfälle auch) brauchen wir natürlich nur einen einzigen Interpreter, und den erzeugen wir oben mittels let myLuaState = luaL_newstate(). Zurückgegeben wird ein Wert vom Typ OpaquePointer, welcher den LuaState referenziert. 

Die Libraries

Mit dem Kern von Lua selbst lässt sich zunächst mal nicht besonders viel anfangen. Lua wird aber zusammen mit einem Set an Standard-Libraries (für Input/Output, Zeichenketten-Manipulation, Mathematik, etc.) ausgeliefert, welches via luaL_openlibs einem bestehenden Lua-State in seiner Gesamtheit hinzugefügt wird. Es ist eine gute Idee, das gleich am Anfang zu machen.

Das Skript

Lua ist eine Skriptsprache, also brauchen wir auch ein Skript, das ausgeführt werden kann. Unser Lua-Skript wird oben in der Stringvariable myScript bereitgestellt.

Für unser Minimum Viable Hello World begnügen wir uns mit einem sehr einfachen Skript, nämlich:

print "Hello, World!"

 

Es gibt bei Lua etliche Methoden, ein LuaState mit ausführbarem Code zu versorgen. Die für uns am besten geeignete Methode ist luaL_loadbufferx (es gäbe im Prinzip noch eine einfachere Methode, hier macht uns aber leider Swift einen Strich durch die Rechnung, dazu jedoch später mehr).

luaL_loadbufferx ist für Swift mittels des Bridging-Headers folgendermaßen definiert:

func luaL_loadbufferx(_ L: OpaquePointer!, _ buff: UnsafePointer!, _ sz: Int, _ name: UnsafePointer!, _ mode: UnsafePointer!) -> Int32

Für diejenigen, auf die dies zunächst einmal gruselig wirkt: Zugegeben, Swift hat leider auch in Version 4 immer noch den großen Fehler, dass es nicht Objective-C ist. Aber das ist in diesem Fall halb so schlimm, die Parameter sind relativ leicht erklärt, und sie sehen in ihrer nativen C-Deklaration auch gleich viel freundlicher aus:


int luaL_loadbufferx (lua_State *L,
                      const char *buff,
                      size_t sz,
                      const char *name,
                      const char *mode);

 

Bei L handelt es sich um einen Zeiger auf unseren LuaState. 

buff ist ein Puffer, der das auszuführende Skript enthält.

sz ist die Größe des auszuführenden Skripts in Bytes.

name ist der „Chunk Name“ des auszuführenden Skripts. Dieser kann zu Debugging-Zwecken verwendet werden, man kann ihn aber auch leer lassen.

mode gibt an, ob Text oder binäre Daten geladen werden sollen (Lua-Programme werden bei der Abarbeitung in Bytecode übersetzt, dieser Bytecode kann auch vom Lua-Compiler direkt erzeugt und mit loadbuffer geladen werden… aber dazu später mehr).  Wird als „mode“ nil übergeben, so ermittelt Lua automatisch den richtigen Modus anhand des Inhalts von buff.

Nun zur Anwendung in unserem kleinen Programm: Swift hat die außerordentlich nette Eigenart, Konvertierungen des „String“-Typs zu einem primitiven C-Character-Array (so wie es Lua benötigt) automatisch vorzunehmen, so dass wir unseren Skript-String einfach ohne kompliziertes Casting als Parameter übergeben können. Nice.

An dieser Stelle eine wichtige Feststellung: luaL_loadbufferx veranlasst Lua dazu, den Inhalt des Puffers (sofern möglich bzw. nötig) zu compilieren, kümmert sich aber nicht um die Ausführung des Codes.

Endlich: Codeausführung!

Zentraler Dreh- und Angelpunkt bei der Abarbeitung von Lua-Code ist der Lua-Stack. So ziemlich alles, was Lua betrifft, spielt sich auf dem Stack ab.

Wird mit luaL_loadbufferx ein Skript geladen, so geschieht in Wirklichkeit das Folgende: Das Skript wird compiliert, und das resultierende Bytecode-Compilat wird als Funktion auf den Lua-Stack gepusht.

Alles, was wir jetzt noch brauchen, ist ein Mechanismus, um die aktuell auf dem Stack liegende Funktion auszuführen. Und genau dazu gibt es lua_callk.

Das automatische Bridging definiert lua_callk so:


func lua_callk(_ L: OpaquePointer!, _ nargs: Int32, _ nresults: Int32, _ ctx: lua_KContext, _ k: lua_KFunction!)

Dabei bedeuten:

L – der Zeiger auf den LuaState

nargs – Anzahl, der Argumente, die auf dem Stack liegen (in unserem Fall: Null. Wir wollen nur die aus dem Skript resultierende Funktion aufrufen)

nresults – Anzahl, der Rückgabewerte. Lua-Funktionen haben die praktische Eigenart, beliebig viele Rückgabewerte liefern zu können. Mit nresults lässt sich beschränken, wieviele Rückgabewerte auf dem Stack liegen sollen. Das heißt, selbst wenn eine Funktion sieben Rückgabewerte liefert, lassen sich diese mit nresults==0 ignorieren oder mit nresults==2 auf die ersten zwei beschränken.  

ctx und k – Diese Parameter haben mit der Verwendung von Coroutinen zu tun. Coroutinen sind ein mächtiges Konzept – um unnötige Verwirrung zu vermeiden, wollen wir sie aber zunächst nicht verwenden. 

Daher ignorieren wir diese Parameter mit der Übergabe von 0 und nil respektive. Was uns auf ein eher unangenehmes Thema bringt, welches wir zum Schluss wenigstens kurz ansprechen müssen:

„Es könnte alles so einfach sein…“

Ich habe es bei der Beschreibung von luaL_loadbufferx schon angedeutet: Lua kennt noch einfachere Aufrufe zum Laden und auch zum Ausführen von Code. 

Wer sich, angespornt durch dieses großartige Posting, im Internetz auf die Suche nach Beispielcode für die C-API von Lua macht, der wird auch früher oder später über die Erwähnung von „luaL_loadbuffer“ und „lua_call“ stolpern, und sich vielleicht sagen: „Hm, bei den AppGuys sieht das viel komplizierter aus, was ist da bloß los?“

Das Problem ist leider, dass uns diese Funktionen unter Swift nichts bringen, denn sie sind nicht wirklich Funktionen sondern Makros. Die Bridging-Fähigkeiten von Swift sind im Prinzip eine sehr feine Sache, sie versagen jedoch, wenn es um C-/ObjectiveC-Makros geht, denn diese sind per Definition eben nicht compiliert und lassen sich somit auch nicht über die Bridge erreichen. 

Im Prinzip gibt es Wege um dieses Hindernis herum, aber die sind allesamt nicht besonders schön. Eine davon werde ich in einem späteren Teil dieser Reihe vorstellen, aber zunächst werden wir uns darauf beschränken, die „mächtigeren“ Aufrufe zu verwenden, auch wenn diese am Anfang etwas komplizierter aussehen.

Zusammenfassung

Wir haben in diesem ersten Teil der Serie gelernt, wie man mit minimalem Aufwand ein Swift-Projekt durch eine Lua-Integration erweitert. Wir haben das Konzept des Lua State kennengelernt und ein paar erste interessante Dinge über den Lua-Stack erfahren. 

Und wir haben ein großartiges Minimum Viable Hello World mit Lua-Integration programmiert, das mit 6.5MB Umfang und einer eingebetteten, vollständigen(!) Lua-Runtime tatsächlich nur geringfügig größer ist als ein natives Swift-Hello-World (bevor jemand fragt: Ja, das ist das optimierte Binary ohne Debugging-Symbole):

…womit auch schon eine 1a-Argumentationshilfe geliefert wäre, wenn die Vermutung im Raum steht, eine Lua-Integration würde das Projekt unnötig aufblähen: Lua ist nicht das, was das Projekt aufbläht 😉

Im nächsten Teil erfahren wir mehr über den Lua-Stack und wie man ihn manipuliert, außerdem lernen wir, wie wir den Code schöner und sicherer machen… und dann geht’s in großen Schritten weiter zu einer App mit Lua. 

Hier erfahren Sie mehr:

Oder rufen Sie uns an: 0221 / 291 993 72

Kommentar verfassen