Awhile ago I started messing with Lua and seeing how I could implement that into my engine. There’s a number of reasons why you would want to add scripting functionality, but for me I wanted to allow users to mod the API set, and to allow faster prototyping. Right now I have about 27 projects that need to be built if I was to do a rebuilt on the entire engine, and that could take 25-45 minutes depending on which computer I’m initiating the build on. Obviously scripting the prototype out first could save me a lot of time so I set out to add scripting to Core. After looking at the more popular scripting languages, I decided to pick Lua. Lua is a very common language in gaming, has great support for C/C++, in fact it was created to work with C and C++.
Before you read this understand, this is not a tutorial on how to use ToLua++, there are plenty of them out there. This is an overview of my process of using it, and how to automate some more of the tedious tasks required before the Lua wrapping code can be created.
I found a tool called ToLua++ that will read your formatted head files and automatically create C++ wrappers to those methods that will allow Lua to access them directly. This is a big time saver because all you need to do is create a simple batch that will run ToLua++ on some specified headers, import the outputted C++ code into your project, and your done (theoretically). It’s not always just punch and run, the ToLua++ tool doesn’t have a full and complete C++ parser, so there are some formatting steps you need to do, but I already coded that up in the “Asset Manager” tool I’ve been working on so I’ll run through some of the logical steps I’ve added.
Check the manual on some of the manual filter you will need to do to your headers, but here is the logic I did to automatically format my own headers…
Here is the tool I made to help automate the entire process:

- The top directory is the root of all my include files.
- After I click on the “Directory List” it will populate all the header files in all child directories.
- From that point I can choose which includes I want to have wrapped, and which header files I want not to include.
- After I click “Generate”, all the ToLua++ pkg files are created and example commands are created so I can simply copy and paste the commands into the command prompt if I want to create the ToLua++ package.
What the generate tool does is it filters the header files I want to be wrapped for Lua, and filters and formats them so ToLua can better read them. Here is an example package file:
class AbstractVehicle : public AbstractLocalSpace { public: ~AbstractVehicle() { } float mass (void) const = 0; float setMass (float) = 0; float radius (void) const = 0; float setRadius (float) = 0; reVector3Df velocity (void) const = 0; float speed (void) const = 0; float setSpeed (float) = 0; reVector3Df predictFuturePosition (const float predictionTime) const = 0; float maxForce (void) const = 0; float setMaxForce (float) = 0; float maxSpeed (void) const = 0; float setMaxSpeed (float) = 0; void update(const float currentTime, const float elapsedTime) = 0; };
This is a class from the AI engine, so now I have this functionality available to me using Lua. It looks like any other C++ class declaration, and about 99% of is, with a few exceptions:
- Pre compiler commands are removed.
- typedef’s are removed.
- Array’s with an element size defined using #define is removed.
- friend declarations are removed.
- using namesapace lines are removed.
- Custom primitive types are replaces with the standard C/C++ names.
- I’ve had problems with virtual in the past so now I just remove the word.
- I remove all private and protected members.
- Finally, ToLua doesn’t seem to play nice with some of my template declarations, so I’ve hardcoded the Asset Manager to find those lines and remove them.
Here is an over all flow of what my tool does when filtering the class declarations:
public void GeneratePackages(CheckedListBox clb, string outputDir, ListBox ingnoreList, TextBox example) { var pkgsCreated = new List<string>(); var di = new DirectoryInfo(outputDir); if (di.Exists) di.Delete(true); di.Create(); example.Text = string.Empty; foreach (string dir in clb.CheckedItems) { var itemsDir = new DirectoryInfo(dir); var tempFileName = outputDir + "\\" + itemsDir.Name + ".temp"; pkgsCreated.Add(tempFileName); using (StreamWriter writer = new StreamWriter(tempFileName)) { example.Text += "tolua++ -n " + itemsDir.Name + " -o " + itemsDir.Name + "_lua.cpp -H " + itemsDir.Name + "_lua.h ./" + itemsDir.Name + " \r\n"; foreach (FileInfo file in itemsDir.GetFiles()) { if (file.Name != "pkg" && !ingnoreList.Items.Contains(file.Name) && (file.Extension == ".h" || file.Extension == ".hpp")) { using (StreamReader reader = new StreamReader(file.FullName)) { while (!reader.EndOfStream) { string line = reader.ReadLine(); // **************** Validate the line ************************* // Many issues with this if (line.Trim().StartsWith("#")) line = string.Empty; // Not needed if (line.Trim().StartsWith("typedef") && !line.Contains("typedef struct")) line = string.Empty; // Variable definition if (line.Contains("NUM_TEAMS")) line = string.Empty; // Doesn't like friends if (line.Trim().StartsWith("friend")) line = string.Empty; // Don't need this if (line.Trim().StartsWith("using namespace")) line = string.Empty; // Has a problem with dereferencing if (line.Contains("** ") && !line.Contains("/*")) line = string.Empty; // Core specfic line = line.Replace("CORE_EXPORT", ""); line = line.Replace("NATURE_EXPORT", ""); line = line.Replace("__declspec(dllexport)", ""); line = line.Replace("virtual", ""); line = line.Replace("f32", "float"); line = line.Replace("s32", "int"); line = line.Replace("u32", "unsigned int"); line = line.Replace("c8", "char"); line = line.Replace("s8", "signed char"); line = line.Replace("u8", "unsigned char"); // AI specific line = line.Replace("template <class Super>", ""); line = line.Replace("template<>", ""); line = line.Replace("template <class ContentType>", ""); line = line.Replace("template< class PathAlike, class Mapping, class BaseDataExtractionPolicy = PointToPathAlikeBaseDataExtractionPolicy< PathAlike > >", ""); line = line.Replace("template< class PathAlike, class Mapping >", ""); line = line.Replace("template< class PathAlike, class Mapping, class BaseDataExtractionPolicy = DistanceToPathAlikeBaseDataExtractionPolicy< PathAlike > > ", ""); line = line.Replace("template< class PathAlike >", ""); if (line.Length > 0) { writer.WriteLine(line); } } } } } } } FilterFiles(pkgsCreated); }
And here is a quick and dirty way to find private and protected members:
private void FilterFiles(List<string> fileList) { foreach (string str in fileList) { var fileName = str.Replace(".temp", ""); using (StreamWriter writer = new StreamWriter(fileName)) { using (StreamReader reader = new StreamReader(str)) { while (!reader.EndOfStream) { string line = reader.ReadLine(); if (line.Trim() == "private:" || line.Trim() == "protected:") { writer.WriteLine(FilterPrivate(reader)); } else { writer.WriteLine(line); } } } } // Delete temp file FileInfo fi = new FileInfo(str); if (fi.Exists) fi.Delete(); } } private string FilterPrivate(StreamReader reader) { // Read through the stream until the private or protected members are skipped. while (!reader.EndOfStream) { string line = reader.ReadLine(); if (line.Trim() == "public:") { return "public:"; } else if( line.Trim() == "};") { return "};"; } } return string.Empty; }
I still have problems with sub classes, but I don’t need 100% API functionality for Lua, so for now I am just excluding them. I’ve got this process down so that I can take 200+ header files, run it through the tool, and execute the command line examples, and have outputted C++ code ready to include in my project without any problems.
Using ToLua++ can be a challenge at first, but once you have a good pipeline down, you’ll be able to make changes to your API, press a few buttons, and those changes can be reflected quickly and correctly in your Lua wrappers.
Here is just a quick example output of ToLua: (it’s not pretty)
/* method: SetYAxisFromTerrain of class Core::CPlayer */ #ifndef TOLUA_DISABLE_tolua_Core_Core_CPlayer_SetYAxisFromTerrain01 static int tolua_Core_Core_CPlayer_SetYAxisFromTerrain01(lua_State* tolua_S) { tolua_Error tolua_err; if ( !tolua_isusertype(tolua_S,1,"Core::CPlayer",0,&tolua_err) || !tolua_isnumber(tolua_S,2,0,&tolua_err) || !tolua_isnoobj(tolua_S,3,&tolua_err) ) goto tolua_lerror; else { Core::CPlayer* self = (Core::CPlayer*) tolua_tousertype(tolua_S,1,0); float elapsedTime = ((float) tolua_tonumber(tolua_S,2,0)); #ifndef TOLUA_RELEASE if (!self) tolua_error(tolua_S,"invalid 'self' in function 'SetYAxisFromTerrain'", NULL); #endif { self->SetYAxisFromTerrain(elapsedTime); } } return 0; tolua_lerror: return tolua_Core_Core_CPlayer_SetYAxisFromTerrain00(tolua_S); } #endif //#ifndef TOLUA_DISABLE