Posted by
| Nick Gammon
Australia (23,120 posts) Bio
Forum Administrator |
Message
| Lua has a provision for extending it by writing your own DLLs, thus making anything possible (eg. opening Windows dialogs etc.).
A brief example follows. The first thing to note is that every function called from Lua has an identical calling pattern:
static int function_name (lua_State *L)
{
/* do something here */
return 0; /* number of results returned */
} /* end of function_name */
You use this "lua state" called L (although you can call it anything of course) to access all Lua data and functions, and to return results.
As an example I will write a small function that calculates miles to kilometres, although in practice you would simply do that in Lua.
static int miles_to_km (lua_State *L)
{
double miles = luaL_checknumber (L, 1);
double km = miles * 1.609;
lua_pushnumber (L, km);
return 1; /* one result */
} /* end of miles_to_km */
The first thing the above does is extract the first argument passed to the function into "miles". If I passed a second argument it would be accessed with (L, 2) instead of (L, 1), and so on.
Then I do some calculations on it.
Finally the result is "pushed" onto the Lua stack (lua_pushnumber). Then the function returns the number 1, to tell Lua that one result is to be returned to the caller.
To make this into a DLL we need a little bit of infrastructure around it. From Lua, when we load a DLL we get to call a single entry point, so if we want to expose more than one function, we need to make a "library" of them. The code below has two functions, the miles-to-kilometre one, and another that calculates the circumference and area of a circle. This second one illustrates returning multiple results:
static int circle_calcs (lua_State *L)
{
double radius = luaL_checknumber (L, 1);
double circumference = radius * 2 * PI;
double area = PI * radius * radius;
lua_pushnumber (L, circumference);
lua_pushnumber (L, area);
return 2; /* two results */
} /* end of miles_to_km */
Next thing we need to do is "register" these functions with Lua, so it knows to add them to its address space. This is done with a small table, that maps function names to their addresses, and then a library call:
static const luaL_reg testlib[] =
{
{"miles_to_km", miles_to_km},
{"circle_calcs", circle_calcs},
{NULL, NULL}
};
/*
** Open test library
*/
LUALIB_API int luaopen_test (lua_State *L)
{
luaL_openlib(L, "test", testlib, 0);
return 1;
}
The function luaopen_test will be our entry point for the DLL. When called it adds miles_to_km and circle_calcs to the "test" library. After that you will be able to do:
k = test.miles_to_km (42)
c, a = test.circle_calcs (16)
We will wrap it all up with a couple of include files, and a definition for LUA_API, which exports the function when the DLL is built:
test.c
#ifdef _WIN32
#define LUA_API __declspec(dllexport)
#endif
#pragma comment( lib, "lua.lib" )
#pragma comment( lib, "lualib.lib" )
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"
#define PI (3.14159265358979323846)
static int miles_to_km (lua_State *L)
{
double miles = luaL_checknumber (L, 1);
double km = miles * 1.609;
lua_pushnumber (L, km);
return 1; /* one result */
} /* end of miles_to_km */
static int circle_calcs (lua_State *L)
{
double radius = luaL_checknumber (L, 1);
double circumference = radius * 2 * PI;
double area = PI * radius * radius;
lua_pushnumber (L, circumference);
lua_pushnumber (L, area);
return 2; /* one result */
} /* end of miles_to_km */
static const luaL_reg testlib[] =
{
{"miles_to_km", miles_to_km},
{"circle_calcs", circle_calcs},
{NULL, NULL}
};
/*
** Open test library
*/
LUALIB_API int luaopen_test (lua_State *L)
{
luaL_openlib(L, "test", testlib, 0);
return 1;
}
The pragma instructions above tell the compiler to link against the lua.lib and lualib.lib files, which expose the entry points in the lua DLLs.
Compile that under Visual C++ 6, and we get a test.dll file output (1.86 Kb, pretty small, huh?).
Copy that dll to the same directory as where MUSHclient is (or our Lua executable is) and we are ready to test.
Start up Lua, or use Lua scripting in MUSHclient.
Next, we load the DLL:
f, e1, e2 = loadlib ("test.dll", "luaopen_test")
This loads our nominated DLL, and tells it to return the exposed entry point luaopen_test as a function.
We can then test how that went:
print (f, e1, e2) --> function: 0062FB50 nil nil
With a bit of luck f will be a function, and the other two arguments (error messages) will be nil. That means it worked.
However if you see this:
nil The specified module could not be found.
open
Then that means the function f was not loaded (is nil) and an error message, and error reason (open). This might be because you didn't put the DLL where it could be found (or the lua.dll or lualib.dll were not found either).
Assuming it loaded OK, then you call the function to actually register the routines in the library:
After you have done that, you can now use the "test" library.
print (test.miles_to_km (40)) --> 64.36
print (test.circle_calcs (15)) --> 94.247779607694 706.8583470577
Note in the second case how we see printed both arguments that were returned. If we want to store them, we could do this:
c, a = test.circle_calcs (15)
[EDIT]
A better way of loading the library is to use "assert" - this checks for a non-nil result. If nil it raises an error giving the error message, if not nil it returns the result. So, the whole load process can be done on one line.
Rather than:
f, e1, e2 = loadlib ("test.dll", "luaopen_test")
if not f then
error (e1)
end -- if
f ()
Do this:
assert (loadlib ("test.dll", "luaopen_test")) ()
The assert assures us we have a function, and the final set of brackets runs the function, thus installing the library. |
- Nick Gammon
www.gammon.com.au, www.mushclient.com | Top |
|