-E
option, but integrating this with Visual Studio is more difficult. In addition, I'll show you how to properly load scripts and modify the PATH lookup variable so your game can have a proper scripts
folder instead of dumping everything in bin
.Compilation
To begin, we need to download LuaJIT and get it to actually compile. Doing this manually isn't too difficult, simply open an x64 Native Tools Command Prompt (or x86 Native Tools if you want 32-bit), navigate tosrc/msvcbuild.bat
and run msvcbuild.bat
. The default options will build an x64 or x86 dll with dynamic linking to the CRT. If you want a static lib file, you need to run it with the static
option. If you want static linking to the CRT so you don't have to deal with that annoying Visual Studio Runtime Library crap, you'll have to modify the .bat file directly. Specifically, you need to find %LJCOMPILE% /MD
and change it to %LJCOMPILE% /MT
. This will then compile the static lib or dll with static CRT linking to match your other projects.This is a bit of a pain, and recently I've been trying to automate my build process and dependencies using vcpkg to act as a C++ package manager. A port of LuaJIT is included in the latest update of vcpkg, but if you want one that always statically links to the CRT, you can get it here.
An important note: the build instructions for LuaJIT state that you should copy the lua scripts contained in
src/jit
to your application folder. What it doesn't mention is that this is optional - those scripts contain debugging instructions for the JIT engine, which you probably don't need. It will work just fine without them.Once you have LuaJIT built, you should add it's library file to your project. This library file is called lua51.lib (and the dll is lua51.dll), because LuaJIT is designed as a drop-in replacement for the default Lua runtime. Now we need to actually load Lua in our program and integrate it with our code. To do this, use
lua_open()
, which returns a lua_State*
pointer. You will need that lua_State*
pointer for everything else you do, so store it somewhere easy to get to. If you are building a game using an Entity Component System, it makes sense to build a LuaSystem
that stores your lua_State*
pointer.Initialization
The next step is to load in all the standard Lua libraries usingluaL_openlibs(L)
. Normally, you shouldn't do this if you need script sandboxing for player-created scripts. However, LuaJIT's FFI library is inherently unsafe. Any script with access to the FFI library can call any kernel API it wants, so you should be extremely careful about using LuaJIT if this is a use-case for your game. We can also register any C functions we want to the old-fashioned way via lua_register
, but this is only useful for functions that don't have C analogues (due to having multiple return values, etc).There is one function in particular that you probably want to overload, and that is the
print()
function. By default, Lua will simply print to standard out, but if you aren't redirecting standard out to your in-game console, you probably have your own std::ostream
(or even a custom stream class) that is sent all log messages. By overloading print()
, we can have our Lua scripts automatically write to both our log file and our in-game console, which is extremely useful. Here is a complete re-implementation of print
that outputs to an arbitrary std::ostream&
object:int PrintOut(lua_State *L, std::ostream& out)
{
int n = lua_gettop(L); /* number of arguments */
if(!n)
return 0;
int i;
lua_getglobal(L, "tostring");
for(i = 1; i <= n; i++)
{
const char *s;
lua_pushvalue(L, -1); /* function to be called */
lua_pushvalue(L, i); /* value to print */
lua_call(L, 1, 1);
s = lua_tostring(L, -1); /* get result */
if(s == NULL)
return luaL_error(L, LUA_QL("tostring") " must return a string to "
LUA_QL("print"));
if(i > 1) out << "\t";
out << s;
lua_pop(L, 1); /* pop result */
}
out << std::endl;
return 0;
}
To overwrite the existing print
function, we need to first define a Lua compatible shim function. In this example, I pass std::cout
as the target stream:int lua_Print(lua_State *L)
{
return PrintOut(L, std::cout);
}
Now we simply register our lua_Print
function using lua_register(L, "print", &lua_Print)
. If we were doing this in a LuaSystem
object, our constructor would look like this:LuaSystem::LuaSystem()
{
L = lua_open();
luaL_openlibs(L);
lua_register(L, "print", &lua_Print);
}
To clean up our Lua instance, we need to both trigger a final GC iteration to clean up any dangling memory, and then we call lua_close(L)
, so our destructor would look like this:LuaSystem::~LuaSystem()
{
lua_gc(L, LUA_GCCOLLECT, 0);
lua_close(L);
L = 0;
}
Loadings Scripts via Require
At this point most tutorials skip to the part where you load a Lua script and write "Hello World", but we aren't done yet. Integrating Lua into your game means loading scripts and/or arbitrary strings as Lua code while properly resolving dependencies. If you don't do this, any one of your scripts that relies on another script will have to dorequire("full/path/to/script.lua")
. We also face another problem - if we want to have a scripts
folder where we simply automatically load every single script into our workspace, simply loading them all can cause duplicated code, because luaL_loadfile
does not have any knowledge of require
. You can solve this by simply loading a single bootstrap.lua
script which then loads all your game's scripts via require
, but we're going to build a much more robust solution.First, we need to modify Lua's
PATH
variable, or the variable that controls where it looks up scripts relative to our current directory. This function will append a path (which should be of the form "path/to/scripts/?.lua"
) to the beginning of the PATH
variable, giving it highest priority, which you can then use to add as many script
directories as you want in your game, and any lua script from any of those folders will then be able to require()
a script from any other folder in PATH
without a problem. Obviously, you should probably only add one or two folders, because you don't want to deal with potential name conflicts in your script files.int AppendPath(lua_State *L, const char* path)
{
lua_getglobal(L, "package");
lua_getfield(L, -1, "path"); // get field "path" from table at top of stack (-1)
std::string npath = path;
npath.append(";");
npath.append(lua_tostring(L, -1)); // grab path string from top of stack
lua_pop(L, 1);
lua_pushstring(L, npath.c_str());
lua_setfield(L, -2, "path"); // set the field "path" in table at -2 with value at top of stack
lua_pop(L, 1); // get rid of package table from top of stack
return 0;
}
Next, we need a way to load all of our scripts using require()
so that Lua properly resolves the dependencies. To do this, we create a function in C that literally calls the require()
function for us:int Require(lua_State *L,const char *name)
{
lua_getglobal(L, "require");
lua_pushstring(L, name);
int r = lua_pcall(L, 1, 1, 0);
if(!r)
lua_pop(L, 1);
WriteError(L, r, std::cout);
return r;
}
By using this to load all our scripts, we don't have to worry about loading them in any particular order - require
will ensure everything gets loaded correctly. An important note here is WriteError()
, which is a generic error handling function that processes Lua errors and writes them to a log. All errors in lua will return a nonzero error code, and will usually push a string containing the error message to the stack, which must then be popped off, or it'll mess things up later.void WriteError(lua_State *L, int r, std::ostream& out)
{
if(!r)
return;
if(!lua_isnil(L, -1)) // Check if a string was pushed
{
const char* m = lua_tostring(L, -1);
out << "Error " << r << ": " << m << std::endl;
lua_pop(L, 1);
}
else
out << "Error " << r << std::endl;
}
Automatic C Binding Generation
Fantastic, now we're all set to load up our scripts, but we still need to somehow define a header file and also load that header file into LuaJIT's FFI library so our scripts have direct access to our program's exposed C functions. One way to do this is to just copy+paste your C function definitions into a Lua file in your scripts folder that is then automatically loaded. This, however, is a pain in the butt and is error-prone. We want to have a single source of truth for our function definitions, which means defining our entire LuaJIT C API in a single header file, which is then loaded directly into LuaJIT. Predictably, we will accomplish this by abusing the C preprocessor:#ifndef __LUA_API_H__
#define __LUA_API_H__
#ifndef LUA_EXPORTS
#define LUAFUNC(ret, name, ...) ffi.cdef[[ ret lua_##name(__VA_ARGS__); ]]; name = ffi.C.lua_##name
local ffi = require("ffi")
ffi.cdef[[ // Initial struct definitions
#else
#define LUAFUNC(ret, name, ...) ret __declspec(dllexport) lua_##name(__VA_ARGS__)
extern "C" { // Ensure C linkage is being used
#endif
struct GameInfo
{
uint64_t DashTail;
uint64_t MaxDash;
};
typedef const char* CSTRING; // VC++ complains about having const char* in macros, so we typedef it here
#ifndef LUA_EXPORTS
]] // End struct definitions
#endif
LUAFUNC(CSTRING, GetGameName);
LUAFUNC(CSTRING, IntToString, int);
LUAFUNC(void, setdeadzone, float);
#ifdef Everglade_EXPORTS
}
#endif
#endif
The key idea here is to use macros such that, when we pass this through the preprocessor without any predefined constants, it will magically turn into a valid Lua script. However, when we compile it in our C++ project, our project defines LUA_EXPORTS
, and the result is a valid C header. Our C LUAFUNC
is set up so that we're using C linkage for our structs and functions, and that we're exporting the function via __declspec(dllexport)
. This obviously only works for Visual Studio so you'll want to set up a macro for the GCC version, but I will warn you that VC++ got really cranky when i tried to use a macro for that in my code, so you may end up having to redefine the entire LUAFUNC
macro for each compiler.At this point, we have a bit of a choice to make. It's more convenient to have the C functions available in the global namespace, which is what this script does, because this simplifies calling them from an interactive console. However, using
ffi.C.FunctionName
is significantly faster. Technically the fastest way is declaring local C = ffi.C
at the top of a file and then calling the functions via C.FunctionName
. Luckily, importing the functions into the global namespace does not preclude us from using the "fast" way of calling them, so our script here imports them into the global namespace for ease of use, but in our scripts we can use the C.FunctionName
method instead. Thus, when outputting our Lua script, our LUAFUNC
macro wraps our function definition in a LuaJIT ffi.cdef
block, and then runs a second Lua statement that brings the function into the global namespace. This is why we have an initial ffi.cdef
code block for the structs up top, so we can include that second lua statement after each function definition.Now we need to set up our compilation so that Visual Studio generates this file without any predefined constants and outputs the resulting lua script to our
scripts
folder, where our other in-game scripts can automatically load it from. We can accomplish this using a Post-Build Event (under Configuration Properties -> Build Events -> Post-Build Event), which then runs the following code:CL LuaAPI.h /P /EP /u COPY "LuaAPI.i" "../bin/your/script/folder/LuaAPI.lua" /YVisual Studio can sometimes be finicky about that newline, but if you put in two statements on two separate lines, it should run both commands sequentially. You may have to edit the project file directly to convince it to actually do this. The key line here is
CL LuaAPI.h /P /EP /u
, which tells the compiler to preprocess the file and output it to a *.i
file. There is no option to configure the output file, it will always be the exact same file but with a .i
extension, so we have to copy and rename it ourselves to our scripts folder using the COPY
command.Loading and Calling Lua Code
We are now set to load all our lua scripts in our script folder viaRequire
, but what if we want an interactive Lua console? There are lua functions that read strings, but to make this simpler, I will provide a function that loads a lua script from an arbitrary std::istream
and outputs to an arbitrary std::ostream
:const char* _luaStreamReader(lua_State *L, void *data, size_t *size)
{
static char buf[CHUNKSIZE];
reinterpret_cast(data)->read(buf, CHUNKSIZE);
*size = reinterpret_cast(data)->gcount();
return buf;
}
int Load(lua_State *L, std::istream& s, std::ostream& out)
{
int r = lua_load(L, &_luaStreamReader, &s, 0);
if(!r)
{
r = lua_pcall(L, 0, LUA_MULTRET, 0);
if(!r)
PrintOut(L, out);
}
WriteError(L, r, out);
return r;
}
Of course, the other question is how to call Lua functions from our C++ code directly. There are many, many different implementations of this available, of varying amounts of safety and completeness, but to get you started, here is a very simple implementation in C++ using templates. Note that this does not handle errors - you can change it to use
lua_pcall
and check the return code, but handling arbitrary Lua errors is nontrivial.template<class T, int N>
struct LuaStack;
template<class T> // Integers
struct LuaStack<T, 1>
{
static inline void Push(lua_State *L, T i) { lua_pushinteger(L, static_cast<lua_Integer>(i)); }
static inline T Pop(lua_State *L) { T r = (T)lua_tointeger(L, -1); lua_pop(L, 1); return r; }
};
template<class T> // Pointers
struct LuaStack<T, 2>
{
static inline void Push(lua_State *L, T p) { lua_pushlightuserdata(L, (void*)p); }
static inline T Pop(lua_State *L) { T r = (T)lua_touserdata(L, -1); lua_pop(L, 1); return r; }
};
template<class T> // Floats
struct LuaStack<T, 3>
{
static inline void Push(lua_State *L, T n) { lua_pushnumber(L, static_cast<lua_Number>(n)); }
static inline T Pop(lua_State *L) { T r = static_cast<T>(lua_touserdata(L, -1)); lua_pop(L, 1); return r; }
};
template<> // Strings
struct LuaStack<std::string, 0>
{
static inline void Push(lua_State *L, std::string s) { lua_pushlstring(L, s.c_str(), s.size()); }
static inline std::string Pop(lua_State *L) { size_t sz; const char* s = lua_tolstring(L, -1, &sz); std::string r(s, sz); lua_pop(L, 1); return r; }
};
template<> // Boolean
struct LuaStack<bool, 1>
{
static inline void Push(lua_State *L, bool b) { lua_pushboolean(L, b); }
static inline bool Pop(lua_State *L) { bool r = lua_toboolean(L, -1); lua_pop(L, 1); return r; }
};
template<> // Void return type
struct LuaStack<void, 0> { static inline void Pop(lua_State *L) { } };
template<typename T>
struct LS : std::integral_constant<int,
std::is_integral<T>::value +
(std::is_pointer<T>::value * 2) +
(std::is_floating_point<T>::value * 3)>
{};
template<typename R, int N, typename Arg, typename... Args>
inline R _callLua(const char* function, Arg arg, Args... args)
{
LuaStack<Arg, LS<Arg>::value>::Push(_l, arg);
return _callLua<R, N, Args...>(function, args...);
}
template<typename R, int N>
inline R _callLua(const char* function)
{
lua_call(_l, N, std::is_void<R>::value ? 0 : 1);
return LuaStack<R, LS<R>::value>::Pop(_l);
}
template<typename R, typename... Args>
inline R CallLua(lua_State *L, const char* function, Args... args)
{
lua_getglobal(L, function);
return _callLua<R, sizeof...(Args), Args...>(L, function, args...);
}
Now you have everything you need for an extensible Lua scripting implementation for your game engine, and even an interactive Lua console, all using LuaJIT. Good Luck!
No comments:
Post a Comment