Lua-Tutorial:
Einbindung von Lua 5.0 anhand von BeispielenLua ist eine kleine, aber feine Script-Sprache, die frei erhältlich und frei verwendbar ist. Der Sourcecode ist unter
http://www.lua.org erhältlich, dort kann man auch diverse Wrapper für Objekte und anderes finden. Das Manual, das dort erhältlich ist, lässt jedoch einiges zu wünschen übrig, und deshalb werden wir
hier jetzt ein paar dieser Schritte hier nachvollziehen.
Lua ist sehr einfach aufgebaut, verbraucht kaum Speicher (100 KB zusätzlich in der EXE-Datei) und ist doch recht mächtig.
Dieses Tutorial geht von Visual Studio .NET 2003 und Lua 5.0 aus. Das Tutorial sollte auf anderen Compilern bzw. IDEs ohne Änderungen laufen. Eventuell unterscheidet sich die Bedienung der IDE.
Einbinden von Lua in das Projekt:Wir beginnen mit dem Erstellen eines neuen Projektes. Wir wählen unter Datei->Neu->Projekt links den Ordner „Visual C++-Projekt“ und rechts „Win32 Konsolenprojekt“. Bestätigen und den Wizard das Projekt erstellen lassen.
Danach in den Projekt-Eigenschaften die Option unter „C++“->„Vorkompilierte Header“ auf „Vorkompilierte Header nicht verwenden“ stellen.
Lua kann als .LIB eingebunden werden, oder aber auch die Source-Dateien direkt verwendet. Da das Erstellen der Libraries etwas aufwendiger ist, und wir uns im Tutorial auf die Einbindung konzentrieren, wählen wir hier die zweite Variante.
Im Projekt-Verzeichnis erstellen wir ein Unterverzeichnis „Lua“. Dorthin kopieren wir aus dem Lua-Paket sämtliche .c und .h-Dateien aus dem „src“-Verzeichnis. Ebenso alle .c-Dateien aus dem „src/lib“-Verzeichnis. Aus dem „include“-Verzeichnis kopieren wir alle .h-Dateien. Das ist insgesamt keine schöne Methode, wird im Lua-Manual jedoch so beschrieben und reicht fürs Erste.
Über Projekt->Neuer Ordner fügen wir dem „Projektmappen-Explorer“ (Solution-Explorer) einen Ordner „Lua“ hinzu. Auf den neuen Ordner rechtsklicken, Hinzufügen->Vorhandenes Element hinzufügen… und dann alle
Dateien im Lua-Unterordner auswählen.
Jetzt sollte man das Projekt ohne Fehler erstellen können. Sind die W64-Warnung angeschaltet, gibt es im Lua-Code ein paar Warnung, diese kann man aber getrost ignorieren (noch arbeiten wir mit 32bit).
Es wird ernst:Um Lua überhaupt verwenden zu können, müssen wir „lua.h“ includen. Da Lua eine C-Library ist, und das Projekt selbst C++, muss man dem Compiler das mitteilen. Das sieht dann so aus:
extern "C"
{
#include "lua/lua.h"
}
Wie jede grössere Library will Lua initialisiert werden. Dazu gibt es den Befehl lua_State* lua_open(); Dieser liefert ein Handle zurück, das für alle weiteren Lua-Befehle nötig ist. Dieses Handle beinhaltet die komplette
Umgebungsstruktur für Lua. Es ist ohne Probleme möglich, mehrere dieser lua_States gleichzeitig geöffnet zu haben. Um dieses Handle wieder freizugeben, gibt es den Befehl
void lua_close( lua_State* ); Dadurch werden sämtliche Variablen und Objekte, die diese Lua-Instanz angelegt hat, freigegeben.
Unser Beispiel-Projekt, erweitert um Lua-Initialisierung und –Freigabe sieht nun so aus:
//LuaTutorial.cpp: Definiert den Einstiegspunkt für die Konsolenanwendung //#include "stdafx.h"
extern "C"
{
#include "lua/lua.h"
}
// eine globale Lua-Instanz
lua_State* g_LuaInstance = NULL;
void init()
{
// eine Lua-Instanz wird initialisiert g_LuaInstance = lua_open();
}
void done()
{
// die Lua-Instanz wird wieder freigegeben lua_close( g_LuaInstance );
}
int _tmain(
int argc, _Tchar* argv[])
{
Init();
// auf einen Tastendruck warten char cDummy;
std::cin >> cDummy;
done();
return 0;
}
Es passiert doch gar nichts?Richtig, im Moment tut unser Beispiel nichts. Lua wird initialisiert, ein Tastendruck auf Enter abgewartet und Lua wieder freigegeben. Lua hat jetzt nur grundlegendste Funktionalität, es reicht aber immerhin zum parsen und Befehle abarbeiten.
Für unser Beispielprogramm benötigen wir noch einige Zusatzbibliotheken, die aber schon mitgeliefert
werden. Dazu müssen noch zwei weitere Includes hinzugefügt werden, "lua/lualib.h"
und "lua/lauxlib.h"
Zusätzlich muss die Zusatzbibliothek noch an der Lua-Instanz angemeldet werden, damit die
Befehle verwendet werden können. Dazu rufen wir nach lua_open ein luaopen_base( g_LuaInstance );auf.
Übrigens, wer in den Source-Code von Lua sieht, wird feststellen, dass dieser Befehl nichts anderes tut, als C-Funktionen an Lua zu binden. Das machen wir im übernächsten Schritt dann selbst.
In diesem Schritt begnügen wir uns damit, eine Text-Datei mit Lua-Befehlen abzuarbeiten. Wir erstellen eine neue Text-Datei, und nennen diese zum Beispiel FirstLua.lua. In diese Text-Datei schreiben wir nur eine Zeile:
Nach dem init()-Aufruf in unserem Programm lassen wir Lua nun diese Datei parsen und abarbeiten. Dazu genügt eine Zeile, lua_dofile( g_LuaInstance, "firstlua.lua" );
Tadaa! Speichern. Erstellen, Starten.
Im Konsolen-Fenster sollte jetzt „Hallo Welt“ stehen und ein Druck auf Enter sollte das Programm wieder beenden.
Es gibt natürlich weitere Methoden, Lua-Befehle abzuarbeiten, es muss nicht aus einer Datei gelesen werden. Mit lua_dostring kann ein übergebener String als Programm abgearbeitet werden.
Der zweite Schritt:Jetzt kommen wir zu dem eigentlich „Sinn“ von Scriptsprachen. Das Aufrufen von Programmfunktionen. Dazu muss man Lua die Funktionen und deren gewünschte Namen zur Verfügung stellen.
Lua erwartet C-Funktionen in einem bestimmten Format. Diese Funktionen erhalten einen Parameter (lua_State*) und geben ein
int zurück. Wir fügen jetzt oberhalb unserer init()-Funktion folgende Zeilen ein:
int VonLua( lua_State* L )
{
std::count << "Von Lua aufgerufen \n";
return 0;
}
Wir erhalten als Parameter in der Funktion einen Zeiger auf die Lua-Instanz, die die Funktion aufgerufen hat, und geben als Return-Wert die Anzahl der Parameter zurück, die wir auf den Lua-Stack (siehe später) geschoben haben; in unserem Fall 0.
Unterhalb des luaopen_base melden wir jetzt diese Funktion an unserer Lua-Instanz an:
lua_pushcfunction( g_LuaInstance, VonLua );
lua_setglobal( g_LuaInstance, "MeineFunktion" );
Wir schieben (pushen) einen Zeiger auf unsere Funktion auf den Lua-Stack
(dazu später mehr) und verpassen der Funktion den Namen „MeineFunktion“. Jetzt
fügen wir in unserer firstlua.lua-Datei noch einen Aufruf von MeineFunktion
ein:
Speichern. Erstellen, Starten. In dem Konsolen-Fenster sollte jetzt ausser „Hallo Welt“ auch noch „Von
Lua aufgerufen“ stehen.
Damit lässt sich schon eine ganze Menge anfangen, aber wir sind noch
nicht am Ende. Wir würden ja auch gerne Variablen austauschen. Wie das geht?
Stacks, Variablen und Schiebung:Lua arbeitet Stack-orientiert und hat auch sämtliche Funktionen darauf optimiert und getrimmt. Wenn man zum Beispiel Variablen übergibt, werden diese auf den Stack geschoben. Derselbe Vorgang mit Rückgabeparametern; diese werden ebenfalls auf den Stack geschoben. Ein riesiger Vorteil gegenüber normalen C/C++-Programmen: Im Gegensatz zu C/C++ ist man nicht auf einen Rückgabeparameter beschränkt, man kann beliebig viele Parameter auf den Stack packen. Ein weiterer Vorteil: Dadurch, dass die Anzahl der Parameter über den Stack festgelegt ist, sind sämtliche an Lua gebundenen C-Funktionen vom Aufbau her identisch. Man könnte diese alle zum Beispiel in eine gemeinsame Liste packen.
Der Nachteil des Ganzen: Die einzelnen Parameter muss man sich Stück für Stück vom Stack fischen. Lua stellt zur Stack-Bearbeitung eine ganze Reihe von Befehlen zur Verfügung. Die Anzahl der Parameter auf dem Stack erhält man über
Man beachte, dass die Anzahl zur Laufzeit nicht fixiert ist, man erhält genau so viele Parameter über den Stack wie beim Aufruf der Funktion übergeben worden sind. Das kann auch variieren.
Um das Ganze sauber zu verarbeiten, sollte man auch den Stack von den Parametern befreien. Dazu verwenden wir den Befehl
lua_pop( luaState*, iAnzahlParameter );
Um sicher zu gehen, codieren wir die Anzahl nicht fest, sondern setzen lua_gettop ein. Das sieht so aus:
lua_pop( L, lua_gettop( L ) );
Für unseren Test gehen wir jetzt nicht genau auf die einzelnen Variablentypen der Parameter ein, sondern lassen uns alle Parameter als String (Text) ausgeben. Wir überprüfen, ob der Parameter überhaupt einen gültigen Wert hat, da der nil-Wert undefiniert ist. Dazu gibt es die Funktion
lua_isnil( luaState*, iStackIndex )
Um einen Parameter als String vom Stack zu holen (Zahlen und andere werden dabei automatisch in Text umgewandelt) benutzen wir die Funktion
lua_tostring( luaState*, -iStackIndex )
In unserem Beispiel bauen wir eine einfache Schleife, die alle Parameter prüft und, falls vorhanden, auf std::cout ausgibt. Das sieht dann so aus:
int VonLua( lua_State* L )
{
int iParameterAnzahl = lua_gettop( L );
std::cout << "Aufgerufen mit" << iParameterAnzahl << "Parametern. \n";
while ( iParameterAnzahl )
{
std::string strParam = "";
if ( !lua_isnil( L, -iParameterAnzahl ) )
{
strParam = lua_tostring( L, -iParameterAnzahl );
std::cout << "Parameter:" << strParam.c_str() << "\n";
} --iParameterAnzahl;
}
lua_pop( L, lua_gettop( L ) );
std::cout << "Von Lua aufgerufen \n";
return 0;
}
Für den Test erweitern wir unser FirstLua.Lua um folgende Zeile:
Meine Funktion( "Ich bin die erste Zeile", 17, "Ich bin das Ende." );
Speichern. Erstellen. Starten.
Unsere Funktion wird jetzt zweimal aufgerufen, einmal mit 0 Parametern,einmal mit dreien.
Unsere Möglichkeiten wachsen und wachsen. Aber noch sind wir nicht am Ende!
Jetzt wollen wir noch Werte zurückgeben! Das ist sogar noch einfacher, als die Werte vom Stack zu holen. Je nach Variablentyp verwenden wir
oder
Es gibt noch einige Typen mehr (z.Bsp. boolean), aber für das Beispiel reichen diese beiden völlig aus.
Wir pushen zwei Parameter auf den Stack, und ändern den Return-Wert auf 2, um Lua die Parameteranzahl mitzuteilen. Unsere C-Funktion sieht jetzt so aus:
int VonLua( lua_State* L )
{
int iParameterAnzahl = lua_gettop( L );
std::cout << "Aufgerufen mit" << iParameterAnzahl << "Parametern. \n";
while ( iParameterAnzahl )
{
std::string strParam = "";
if ( !lua_isnil( L, -iParameterAnzahl ) )
{
strParam = lua_tostring( L, -iParameterAnzahl );
std::cout << "Parameter:" << strParam.c_str() << "\n";
}
--iParameterAnzahl;
}
lua_pop( L, lua_gettop( L ) );
std::cout << "Von Lua aufgerufen \n";
lua_pushstring( L, "Hurz" );
lua_pushnumber( L, 42 );
return 2;
}
Unser Script läuft zwar so, wie es im Moment aussieht, aber wir möchten ja unsere beiden Parameter auch sehen. Werden mehrere Parameter zurückgegeben, sieht der Aufruf im Script so aus:
ReturnWert1, ReturnWert2 = Funktion( Aufruf-Parameter );
Wir ändern unser Script ab und bauen auch eine Ausgabe ein:
print "Hallo Welt";
MeineFunktion();
Wert1, Wert2 = MeineFunktion( "Ich bin die erste Zeile", 17, "Ich bin das Ende." );
print( "Wert1: ", Wert1 );
print( "Wert2: ", Wert2 );
Auf die Gefahr der Wiederholung hin:
Speichern. Erstellen. Starten. Hurrrrra, Kasperl!
Damit lässt sich jetzt schon sehr viel machen. Weiterführende Schritte wären jetzt tatsächlich Objekte ansprechen bzw. fortgeschrittene Themen wie coroutines, eine Art Threads im Script.
Vielen Dank an Georg Rottensteiner für dieses Tutorial!