Lua in C einbetten


< Metatabellen und objektorientierte Programmierung
Gesamtübersicht (Start) / Seminarthemen WS 2009/10

Übersicht


Allgemeines zum Einbetten von Lua in C

Beim Einbetten von Lua in C müssen eine Reihe von Differenzen zwischen den Sprachen überwunden werden. Dazu zählen unter anderem die Speicherverwaltung (automatisch gegen manuell), die Art der Typisierung (dynamisch gegen statisch) und ganz generell die Datentypen. Die Lösung für diese Probleme ist ein Stack über den kommuniziert wird. Lua und C können Daten von diesem Stack lesen und auch wieder welche darauf ablegen. Die Folge ist, dass C Lua Funktionen nutzen kann und Lua ebenso C Funktionen. Folgendes Beispiel zeigt, wie man ein einfaches Lua Skript aus C heraus ausführen kann:

C-Code:
#include <stdio.h>
#include "lua.h"       /* Kernfunktionen */
#include "lauxlib.h"   /* Hilfsfunktionen */
#include "lualib.h"    /* die Bibliotheken */

int main (int argc, char *argv[])
{
	int result;
	lua_State *L = luaL_newstate();
	luaL_openlibs(L);
	result = (luaL_loadfile(L, "script.lua") || lua_pcall(L, 0, 0, 0));
	if (result){
		fprintf(stderr, "Fehler beim Ausführen des Skripts !\n");
	}
	fprintf (stdout, "Ausgabe von C\n");
	lua_close(L);
	return 0;
}
Lua-Code:
print("Ausgabe von Lua")
Zunächst wird mit luaL_newstate ein neuer Lua Zustand erzeugt. Darin ist die komplette Umgebung gekapselt. Mit luaL_openlibs werden alle Standardbibliotheken geöffnet (Mathe-Bibliothek, String-Bibliothek, ...). luaL_loadfile öffnet dann das Skript, kompiliert es und legt das Ergebnis auf dem Stack ab. lua_pcall führt dieses Ergebnis, welches auf dem Stack liegt, schließlich aus. Am Ende wird der Zustand mit lua_close geschlossen.

Wird dieses Programm ausgeführt steht auf der Standardausgabe:
Ausgabe von Lua
Ausgabe von C

Der Stack

Es gibt eine ganze Reihe von Funktionen mit denen der Stack verändert und gelesen werden kann. Hier kurz nur die für den Anfang wichtigsten Funktionen:
/* Eine Umgebung (Zustand) *L muss bereits erzeugt worden sein */

/*Beispiele für Funktionen, die Werte auf dem Stack ablegen */
lua_pushnil(L);
lua_pushboolean(L, 0);
lua_pushnumber(L, 5.6);
lua_pushinteger(L, 19);
lua_pushstring(L, "Hallo Welt");

/* Beispiele für Funktionen, die Werte vom Stack holen */
/* Der Parameter index gibt die Position im Stack an */
int lua_toboolean (lua_State *L, int index);
lua_Integer lua_tointeger (lua_State *L, int index);
lua_Number lua_tonumber (lua_State *L, int index);
const char *lua_tostring (lua_State *L, int index);

/* Beispiele für Funktionen mit denen der Typ eines Wertes an einer Position auf dem Stack geprüft werden kann */
int res;
res = lua_isnil(L, 1);    /* das unterste Element (zuerst abgelegt) */
res = lua_isnumber(L, 1); /* das oberste Element (zuletzt abgelegt) */
res = lua_istable(L, 4);
res = lua_isstring(L, 4);
Welcher Wert für index zum indizieren des Stacks angegeben werden muss, ist der folgenden Tabelle zu entnehmen:

Wert des Stacks (Position)positiver Indexnegativer Index
"Erster Wert" (unten)1-4
"Zweiter Wert"2-3
"Dritter Wert"3-2
"Vierter Wert" (obenen)4-1


Lua-Funktionen aus C heraus aufrufen

Um in einem C Programm eine in Lua geschrieben Funktion zu benutzen ist folgendes Schema anzuwenden:
  1. Das Lua-Skript mit der gewünschten Funktion ausführen.
  2. Die Funktion auf dem Stack ablegen.
  3. Die Argumente der Funktion auf den Stack ablegen.
  4. Die auf dem Stack liegende Funktion ausführen.
  5. Die Ergebnisse vom Stack abholen.
Das folgende Beispiel demonstriert dieses Vorgehen an Hand einer Lua Funktion zum Berechnen des ggT, die in C genutzt werden soll.

Lua Code:
function ggT(n, m)
	q = math.floor(n/m)
	r = n - m * q
	if r==0 then return m else return ggT(m,r) end
end
C-Anwendung:
#include <stdio.h>
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"


int main (int argc, char *argv[])
{ 
   int m = 36, n = 27, result;
   lua_State *L = luaL_newstate();
   luaL_openlibs(L);
   result = luaL_loadfile(L, "script.lua") || lua_pcall(L, 0, 0, 0);
   if (result){
      fprintf(stderr, "Fehler beim Laden des Skripts: %s!\n", lua_tostring(L, -1));
   }

   lua_getglobal(L, "ggT");
   lua_pushnumber(L, m);
   lua_pushnumber(L, n);

   if (lua_pcall(L, 2, 1, 0))
      fprintf(stderr,"Fehler in ggT: %s\n", lua_tostring(L, -1));

   fprintf (stdout, "Der ggT von %d und %d ist %d\n",m ,n , lua_tointeger(L, -1));

   lua_close(L);  
   return 0;
}

Hier wird zunächst mit lua_getglobal die ggT Funktion auf dem Stack abgelegt. Anschließend werden mit lua_pushnumber die beiden Argumente der Funktion auf dem Stack abgelegt. lua_pcall führt diese Funktion dann aus. Dabei gibt das zweite Argument von lua_pcall die Anzahl der Argumente der auf dem Stack liegenden Funktion an und das dritte Argument die Anzahl der Rückgabewerte. Das Ergebnis von ggT landet wieder auf dem Stack, wo es mit lua_tointeger abgeholt wird.

Lua-Skripte als Konfigurationsdateien nutzen

Um Anwendungen mit Lua zu konfigurieren, kann eine besonders erwähnenswerte Methode genutzt werden. Dabei versteht man die globalen Variablen als Attribute deren Werte gesetzt werden. Diese Attribute (globale Variablen) werden dann in einem C-Programm ausgelesen. Als Beispiel stelle man sich eine Anwendung mit mehreren Fenstern vor, die man konfigurieren möchte.

Lua-Skript:
title = "Ein Fenstertitel"
icon = "data/icon.ico"
width = 600
height = 400
C-Anwendung:
#include <stdio.h>
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"


int main (int argc, char *argv[])
{ 
   int result;
   lua_State *L = luaL_newstate();
   luaL_openlibs(L);
   result = luaL_loadfile(L, "script.lua") || lua_pcall(L, 0, 0, 0);
   if (result){
      fprintf(stderr, "Fehler beim Laden des Skripts: %s!\n", lua_tostring(L, -1));
   }

   lua_getglobal(L, "title"); /* Wert von title auf dem Stack ablegen */
   lua_getglobal(L, "icon");
   lua_getglobal(L, "width");
   lua_getglobal(L, "height");
 	
   if (lua_isstring(L,-4) && lua_isstring(L, -3) && lua_isnumber(L, -2) && lua_isnumber(L,-1))
   {
      fprintf (stdout, "Fenstertitel: %s\n",lua_tostring(L, -4));
      fprintf (stdout, "Icon Pfad: %s\n",lua_tostring(L, -3));
      fprintf (stdout, "Breite: %d\n",lua_tointeger(L, -2));
      fprintf (stdout, "Höhe: %d\n",lua_tointeger(L, -1));
   }
   else
   {
      fprintf (stdout, "Fehlerhafte Konfigurationsdatei!\n");
   }

   lua_close(L);  
   return 0;
}
Hier werden im Lua_Skript die Eigenschaften des Fensters festgelegt, welche dann in C mit lua_getglobal (legt den Inhalt einer globalen Variable auf dem Stack ab) ausgelesen werden. Das Tolle an dieser Vorgehensweise ist, dass man die Eigenschaften ändern kann ohne die Anwendung neu kompilieren zu müssen, man kann es sich also sparen einen eigenen Parser zu schreiben. Natürlich können auch beliebige Berechnungen im Skript durchgeführt werden. Zur Krönung könnte die C-Anwendung im Voraus schon Informationen an das Skript übermitteln, wie z.B. das Seitenverhältnis des Bildschirms.

C-Funktionen aus Lua heraus aufrufen

Das oben gezeigte Beispiel lässt sich auch umdrehen. Statt C um eine Lua Funktion zu erweitern, kann auch Lua um C Funktionen erweitert werden.

C-Anwendung:
#include <stdio.h>
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"
#include <math.h>


static int ggT1(int n, int m)
{
   int q = floor(n / m);
   int r = n - m * q;
   if (r==0)
      return m;
   else
      return ggT1(m,r);
}

/* Diese Funktion wird Lua zur Verfügung gestellt */
static int ggT (lua_State *L){
   int n = lua_tointeger(L, 1); /* 1. Paramter vom Stack lesen */
   int m = lua_tointeger(L, 2); /* 2. Paramter vom Stack lesen */
   lua_pushnumber(L, ggT1(n, m)); /* Ergebnis mit ggT1 berechnen und auf den Stack legen */
   
   return 1; /* Wie viele Rückgabewerte hat die Funktion */
}

int main (int argc, char *argv[])
{ 
   int result;
   lua_State *L = luaL_newstate();
   luaL_openlibs(L);
   lua_pushcfunction(L, ggT); /* Die Funktion auf dem Stack ablegen */
   lua_setglobal(L, "ggT");  /* Die auf dem Stack liegende Funktion unter der Variable ggT verfügbar machen */
   result = luaL_loadfile(L, "script.lua") || lua_pcall(L, 0, 0, 0);
   if (result){
      fprintf(stderr, "Fehler beim Ausführen des Skripts: %s!\n", lua_tostring(L, -1));
   }

   lua_close(L);  
   return 0;
}
Die Funktion welche Lua zur Verfügung gestellt werden soll ist hier ggT. Da C-Funktionen für Lua immer die hier genutzte Signatur haben müssen, wurde eine zweite Funktion ggT1 erstellt, die die eigentliche Berechnung durchführt. ggT liest zunächst die Argumente vom Stack (Fehler werden hier nicht abgefangen !), führt dann die Berechnung aus und legt das Ergebnis auf dem Stack ab. Am Ende muss die ggT Funktion die Anzahl der Werte zurückgeben, die sie wieder auf dem Stack abgelegt hat. Dies ist notwendig, da eine Lua-Funktion mehrere Rückgabewerte haben kann und man solche Funktionen natürlich auch in C erstellen möchte.
Bevor das Skript, welches die neue Funktion benutzen soll, ausgeführt werden kann muss mit lua_pushcfunction die Funktion auf dem Stack abgelegt werden. Dann wird diese mit lua_setglobal unter der globalen Variable ggT verfügbar gemacht. Jetzt kann die neue Funktion in Lua genutzt werden.

Ein Beispielskript, welches die neue Funktion nutzt:
io.write("Berechnung des ggT\n")
io.write("Erste Zahl ? ")
n = io.read()
io.write("Zweite Zahl ? ")
m = io.read()
io.write("Der ggT von ",n ," und ", m, " ist ", ggT(n, m), "\n")

Benutzerdefinierte C-Typen in Lua nutzen

Bis jetzt konnten wir nur Werte, die von einem vordefinierten Typ sind, an Lua übergeben und von dort abholen. Um auch eigene C-Typen an Lua zu übergeben gibt es das Konzept der Userdata. Dabei wird zwischen Full-Userdata und Light-Userdata unterschieden. Full_Userdata allokieren einen Speicherbereich der von uns genutzt werden kann. Dieser Speicherbereich steht unter Kontrolle der automatischen Speicherverwaltung von Lua. Man kann es sich in etwa als ein "Lua-malloc" vorstellen.
Bei Light-Userdata müssen wir uns selbst um die Speicherveraltung kümmern. Lua wird lediglich ein Zeiger übergeben.
Für Lua sind Userdata nur Container, die als Blackbox behandelt werden. Mithilfe von Metatabellen kann man jedoch Operationen auf diesen Containern definieren. Metatabellen helfen auch dabei mehrere Userdata voneinander zu unterscheiden.

In folgenden Beispiel wollen wir Vektoren von der Dimension 3 in C mit Hilfe von Userdata implementieren. Dabei werden wir im Gegensatz zu den vorherigen Beispielen keine C Host-Anwendung erstellen, sondern eine C-Bibliothek, die in ein Lua-Skript eingebunden werden kann:
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"
#include <math.h>

/* Die Struktur eines Vektors der Dimension 3 */
typedef struct vec3{
	float x;
	float y;
	float z;
} vec3;


/* Einen neuen Vektor erzeugen */
static int new(lua_State *L){
	vec3 *res;
  	
	/* Prüfen, ob Argumente gültig sind */
	if (lua_isnumber(L, 1) && lua_isnumber(L, 2) && lua_isnumber(L, 3))
	{
		/* Argumente vom Stack lesen */
		float x = lua_tonumber(L, 1);
  		float y = lua_tonumber(L, 2);
  		float z = lua_tonumber(L, 3);
	
		/* Ein neues Full-Userdata erzeugen */
		res = (vec3 *)lua_newuserdata(L, sizeof(vec3));
		res->x = x;
		res->y = y;
		res->z = z;

		/* Dem neuen Userdata die selbe Metatabelle, die alle Vektoren haben, zuordnen */
		luaL_getmetatable(L, "LuaSeminar.vec3");
		lua_setmetatable(L, -2);	

		return 1; /* Wie viele Rückgabewerte */
	}
	else
	{
		return luaL_error(L, "Fehlerhafter Aufruf von newvec3");
	}
}


/* Zwei Vektoren addieren */
static int add(lua_State *L){
	vec3 *res;

	/* Prüfen, ob richtige Userdata auf dem Stack liegen und lesen */
	/* Fehlerbehandluung fehlt !!! */
	vec3 *a = luaL_checkudata(L, 1, "LuaSeminar.vec3");
	vec3 *b = luaL_checkudata(L, 2, "LuaSeminar.vec3");
  
	/* Das Ergebnis erzeugen */
	res = (vec3 *)lua_newuserdata(L, sizeof(vec3));
	res->x = a->x + b->x;
	res->y = a->y + b->y;
	res->z = a->z + b->z;	
	
	/* Wieder die Metatabelle zuordnen */
	luaL_getmetatable(L, "LuaSeminar.vec3");
	lua_setmetatable(L, -2);	

	return 1; /* Wie viele Rückgabewerte */
}


/* Stringrepräsentation des Vektors erzeugen */
static int tostring(lua_State *L){
	/* Prüfen, ob richtige Userdata auf dem Stack liegen und lesen */
	/* Fehlerbehandluung fehlt !!! */
	vec3 *v = luaL_checkudata(L, 1, "LuaSeminar.vec3");
	
	/* Das Ergebnis auf dem Stack ablegen */
	lua_pushfstring(L, "(%f, %f, %f)", v->x, v->y, v->z);
	
	return 1; /* Wie viele Rückgabewerte */
}


/* Einträge für die Tabelle, die in Lua zugreifbar sein wird */
/* Array von Paaren aus Namen und Funktionszeiger */
static const struct luaL_Reg vec3_func [] = {
	{"new", new}, /* Neuen Vektor erzeugen */
	{NULL, NULL}  /* Markierung für das Ende */
};


/* Einträge für die Metatabelle */
/* Array von Paaren aus Namen und Funktionszeiger */
static const struct luaL_Reg vec3_meta [] = {
	{"__add", add},           /* Metamethode zum addieren */
	{"__tostring", tostring}, /* Metamethode um Stringrepräsentation zu erzeugen */
	{NULL, NULL}              /* Markierung für das Ende */
};


/* Wird aufgerufen, wenn Bibliothek geöffnet wird */
int luaopen_vec3 (lua_State *L) {
	/* Eine Metatabelle unter dem Namen LuaSeminar.vec3 im Register erzeugen */
	luaL_newmetatable(L, "LuaSeminar.vec3");
	
	/* Eintragungen in Metatabelle durchführen */
	luaL_register(L, NULL, vec3_meta);
	
	/* Eintragungen in der neuen Tabelle für Vektoroperationen (in Lua zugreifbar) */
	luaL_register(L, "vec3", vec3_func);
	
	return 1;
}



Ein Lua-Skript, welches die Bibliothek nutzt:
require "vec3"

a = vec3.new(1, 2, 3)
b = vec3.new(23, 5 ,17)
c = a + b

print(a)
print("+")
print(b)
print("=")
print(c)

Wenn mit require im Lua-Skript die Bibliothek geöffnet wird, wird die Funktion luaopen_vec3 aufgerufen, welche zunächst eine Metatabelle mit dem Bezeichner LuaSeminar.vec3 im sogenannten Register erstellt. Dieses Register ist ein Ort für globale Variablen. Darin will man Werte in der Bibliothek zwischen mehreren Funktionsaufrufen speichern. Weitere Details würden hier zuweit führen und ich kann nur auf die angegebene Literatur verweisen.
Jedenfalls soll diese Metatabelle dazu dienen, die Vektoren mit dieser Metatabelle zu verknüpfen, um so eine Zusammengehörigkeit zu definieren. luaopen_vec3 trägt anschließend noch die Metamethoden in der Metatabelle ein. Als letzter Schritt wird in der Funktion die Tabelle vec3 in Lua verfügbar gemacht und gleichzeitig die gewünschten Methoden (hier new) darin eingetragen.
Alles Weitere sollten Sie der Inline-Dokumentation entnehmen können.

< Metatabellen und objektorientierte Programmierung
Zum Seitenanfang