• Announcement: Lua.org now officially recommends this forum as a meeting place for the Lua community

Lua, Sol 3 and C++ (Oh, My!): Part Deux - We Like Our Toes. (1 Viewer)

Lua, Sol 3 and C++ (Oh, My!): Part Deux - We Like Our Toes.​


1607493930703.png

Welcome Back Kids

Wow, you came back! Maybe you really do want to learn to use Lua with C++? If you managed to stick through the first part of my series on using Sol 3 then you just might have the pain threshold that suites true C++ developers. If you haven't read the first part (and liked it damnit!) then you're a nancy boy and you need to go back (and at least upvote it. he he.).

I'm going to assume you didn't go back and read the first part because you're a programmer looking for free advice on the internet and you have the patience of a hummingbird (nancy boy). Here is the recap:

Sol 3 is a header only Lua interface that takes advantage of templates and the C++17 standard to obfuscate the boundaries of C++ and Lua. To use Sol 3, just clone the git repo and include the sol.hpp header file (and an empty sol/config.hpp) in your project and you're off. The documentation is here and here. ThePHD is not a doctor but he is a "good" wizard and he can help you with your questions (and there will be many, this is C++).

Sooo, now whut?

In the first article we used Sol 3 with WinLua Toolchain to compile a friendly little program that barked like a dog. As exciting as that is, this next article will expand on the functionality of Sol 3 by fleshing out a C++ application, starting with one of the most important components: configuration files. If you don't believe me about the importance of config files, then I suggest you have never deployed an application. It is always the first thing I add to an application (and I never tell my manager) and that practice has saved my ass on numerous occasions. Like the time the client changed a software requirement and everyone else freaked out but I played the cool programmer and said: "sure, no problem" because I had put all the settings in a config file and hadn't told anyone. Besides, everything you hear on the internet is true.

To the Bat Cave, Robin
(nananananana batman!)

You don't need to use WinLua or Xmake or any of the other tools I'm using (other than C++, Lua and Sol 3 of course). Sol 3 works with all the compilers on most platforms: GCC, LLVM (Clang), Mingw-64, Microsoft VC++, and llvm-mingw. However, WinLua Toolchain is a derivative of llvm-mingw and contains xmake and Sol 3 which make it a (conveniently!) compelling choice.

Let's open up our little project from the first article in VS Code again.

luasol-proj-1.png

We are going to add two new files: appConfig.cpp and include/appConfig.hpp.

include/appConfig.hpp
C++:
#ifndef _LUASOL_CONFIG_H_
#define _LUASOL_CONFIG_H_
#include <string>
#include "sol.hpp"
class appConfig
{
private:
    /* data */
    std::string m_fileName;
    std::string m_projectName;
    std::string m_projectType;
    sol::table m_compilerOptions;
    sol::state m_lua;
    bool initLua(std::string *errMsg);
public:
    appConfig (std::string filename);
    ~appConfig ();
};
#endif //_LUASOL_CONFIG_H_

appConfig.cpp
C++:
#include "appConfig.hpp"
appConfig::appConfig(std::string filename)
{
    std::string msg;
    m_fileName = filename;
    if(!initLua(&msg))
    {
        std::cout << msg <<std::endl;
    }
    m_lua.script("print('bark bark bark!')");
}
appConfig::~appConfig()
{
}
bool appConfig::initLua(std::string *errMsg)
{
    m_lua.open_libraries(sol::lib::base, sol::lib::package, sol::lib::string, sol::lib::table);
    sol::protected_function_result res = m_lua.do_file(m_fileName);
    if(!res.valid())
    {
        if(errMsg)
        {
            *errMsg = "Failed to load the file";
        }
        return false;
    }
    return true;
}

Okay, lets talk about what we are doing in initLua. One of the beautiful things about Sol is it allows us to make protected calls. That means if our scripts go pear shaped, we are safe and the Lua stack doesn't unwind. While it is possible - and indeed very easy - to call Lua functions willy-nilly with sol, if we use the provided C++ abstractions, we are able to safely work with our Lua state with relative ease. In our case here, we are running Lua's dofile command to execute the file passed in by our constructor. If something goes hinky in our file, our state is safe. As with many things in C++, that last statement about "being safe" is only true sometimes, but we're going to run with that for now.


You will probably note that we have a really lame "print line" and "bark script" in our constructor. That is a temporary measure to get us a working file in as few lines as possible.


Let's gut our main.cpp and add our new class:

C++:
#include <iostream>
#include "appConfig.hpp"

int main(int, char*[])
{
    appConfig conf("proj1.lua");
    return 0;
}

And now we are going to add our new appConfig.cpp file to the build.

Lua:
-- define target
target("main")
    -- set kind
    set_kind("binary")
    -- add files
    add_files("main.cpp", "appConfig.cpp")  --< woot woot! Config train comin through

    add_includedirs("include", "C:\\Program Files (x86)\\WinLua\\Lua\\5.3\\include")
    add_linkdirs("C:\\Program Files (x86)\\WinLua\\Lua\\5.3\\bin")
    add_links("lua53")
    add_cxflags("-std=c++17")



VS Code Note:

If you are using VS Code it is likely giving you a bunch of read squiggly lines on your include statements. That's because VS Code needs it's own configuration for the intelliSense. We can fix the intelliSense by updating the .vscode/c_cpp_properties.json file created by VS Code. Normally VS Code creates that file but it's inconsistent. If you don't have the file .vscode/c_cpp_properties.json in your project, hit ctrl+shift+p (command palette) and type "C/C++: Edit Configurations (JSON)". That will create and open an new file.

We need to add the same include paths that are in our xmake.lua file to the c_cpp_properties.json file. Here is my file:

Code:
{
    "configurations": [
        {
            "name": "Win32",
            "includePath": [
                "C:/Program Files (x86)/WinLua/Lua/5.3/include/",
                "${workspaceFolder}/include/"
            ],
            "defines": [
                "_DEBUG",
                "UNICODE",
                "_UNICODE"
            ],
            "compilerPath": "C:/Program Files (x86)/WinLua/WLC/bin/clang++.exe",
            "cStandard": "c17",
            "cppStandard": "c++17",
            "intelliSenseMode": "clang-x86"
        }
    ],
    "version": 4
}

If the intellisense doesn't clear up and you've tested your include paths, then try restarting VS Code. I've had that help on more than one occassion.


Build it!
Since we installed the VS Code extension for XMake in the first article (nancy boy! Go read it!) we can press the build icon at the bottom of our VS Code editor, or we can use the "command palette" (ctrl+shift+p) and type `xmake build`


xmake-build-bar.png


I have updated the key bindings in Manage->Keyboard Shortcuts and rearanged them as follows:

New window ctrl+shift+n -> ctrl + alt + n (re-assigned)
Build Task -> (removed)
xmake: build -> ctrl+shift+b (same as Visual Studio)
xmake: clean -> ctrl+shift+n
xmake: run -> shift + .

I'm on a laptop and these key bindings keep my hands in the middle of the computer. Okay, enough chit chat. Let's build!

appconfig-build-ok.png

Excellent Smithers. Let's run it.

appconfig-no-file.png

Okay, this is good. What is this telling us? If we review our code, we will note that in main() we constructed a config object and passed a file name of "proj1.lua". We ran Lua and failed to pass in a valid file name because the "proj1.lua" file doesn't exist. But it also tells us something else. Our Lua state is still valid and ran another script. That's because do_file was run as a protected function call, and so Lua didn't puke it's guts out unwind it's stack. If one looks then to the call `m_lua.script("print('bark bark bark!')");` and uses intellisense we will see that it too returns a protected_function_result. We can see that Sol 3 calls are by default using the Lua pcall system to protect us from shooting ourselves in the foot. In a future article, we will talk about turning that off in the config.hpp file. For now, we are very happy to have that safety turned on. We like our toes.

Filling It In

So, we are going to ramp this up a little bit and finish off our config file system. Since I've been dealing with compilers and IDEs and such, I'm going to create a configuration system for a new IDE that doesn't exist. We are going to:
  • Create a new Lua config file
  • Update xmake to copy the file into our build directory
  • Fill out our appConfig class with some functionality
  • Update main.c to use our new config file.
First, our "proj1.lua" file:

Lua:
project_name = "ASwingAroundSol"
project_type = "C++ (yuck)"
compiler_options = {
    "-std=c++17",
    "-Wno-deprecated",
    "-ffast-math"
}

Now we are going to update our xmake.lua file to copy the config file on build.

Lua:
-- define target
target("main")
    -- set kind
    set_kind("binary")
    -- add files
    add_files("main.cpp", "appConfig.cpp")
    add_includedirs("include", "C:\\Program Files (x86)\\WinLua\\Lua\\5.3\\include")
    add_linkdirs("C:\\Program Files (x86)\\WinLua\\Lua\\5.3\\bin")
    add_links("lua53")
    add_cxflags("-std=c++17")
  
    after_build(function(target)
        os.cp("proj1.lua", path.join(target:targetdir(),"proj1.lua"))
    end);

And now our new appConfig.hpp and cpp files:

C++:
#ifndef _LUASOL_CONFIG_H_
#define _LUASOL_CONFIG_H_
#include <string>
#include "sol.hpp"
class appConfig
{
private:
    /* data */
    std::string m_fileName;
    std::string m_projectName;
    std::string m_projectType;
    sol::table m_compilerOptions;
    sol::state m_lua;
    bool initLua(std::string *errMsg);
public:
    appConfig (std::string filename);
    ~appConfig ();
    void getProjectName(std::string *projectName);
    void getProjectType(std::string *projectType);
    void getCompilerOptionString(std::string *options);
    int setCompilerOptions(std::string options);
};
#endif //_LUASOL_CONFIG_H_

C++:
#include "appConfig.hpp"
appConfig::appConfig(std::string filename)
{
    std::string msg;
    m_fileName = filename;
    if(!initLua(&msg))
    {
        std::cout << msg <<std::endl;
        return;
    }
    sol::optional<std::string> n = m_lua["project_name"];
    m_projectName = n.value_or("New Project");
  
    sol::optional<std::string> m = m_lua["project_type"];
    m_projectType = m.value_or("LuaProject");
    sol::optional<sol::table> t = m_lua["linker_options"];
    if(t.has_value())
    {
        m_linkerOptions = t.value();
    }
}
appConfig::~appConfig()
{
}
bool appConfig::initLua(std::string *errMsg)
{
    m_lua.open_libraries(sol::lib::base, sol::lib::package, sol::lib::string, sol::lib::table);
    sol::protected_function_result res = m_lua.do_file(m_fileName);
    if(!res.valid())
    {
        if(errMsg)
        {
            *errMsg = "Failed to load the file";
        }
        return false;
    }
    return true;
}

void appConfig::getProjectName(std::string *projectName)
{
    *projectName = m_projectName;
}
void appConfig::getProjectType(std::string *projectType)
{
    *projectType = m_projectType;
}
  
void appConfig::getCompilerOptionString(std::string *options)
{
     sol::optional<std::string> opts = m_lua.script(R"(
        local opts = nil
        if compiler_options then
            for i,v in pairs(compiler_options) do
                if not opts then
                    opts = v
                else
                    opts = string.format('%s %s', opts or "", v)
                end
            end
        end
        return opts
    )");
    if(opts.has_value())
    {
        *options = opts.value();
    }
}

Lastly let's update `main()`

C++:
#include <iostream>
#include "appConfig.hpp"
int main(int, char*[])
{
    appConfig conf("proj1.lua");
    std::string name;
    std::string type;
    std::string opts;
    conf.getProjectName(&name);
    conf.getProjectType(&type);
    conf.getCompilerOptionString(&opts);
    std::cout << name << std::endl;
    std::cout << type << std::endl;
    std::cout << opts << std::endl;
    return 0;
}

If we run our code, we get a very satisfying output:

config-output-success.png
Let's look at our new constructor in appConfig.cpp. We see a brilliant little device called sol:: optional<>. This mechanism lets us request anything we want from Lua and access it safely. In our constructor we access two strings and a sol::table object. The table object assignment demonstrates how to safely pull a table into C++.

The next interesting piece of code is the function getCompilerOptionsString. This function uses lua.script to pull in a string representation of table contents. It's very possible for us to write a function that would cause an interpreter error. Lua.script can be accessed as a protected_function_result, or as an sol:: optional<>. I'm not sure the mechanisms yet and it seems a bit like witchcraft, but it sure makes it simple to run in protected mode and safely access return values. I'll update the article as I gain more insight.

Conclusion (Finally! sheesh)
In this article we have seen how we can use the default safety mechanisms in Sol 3 to access Lua through the pcall system with little effort on our part. While the pcall system does impart a small performance overhead, the consequences of not using it are far less appealing. Sol does provide overrides for turning the safety's off, but that is for a future article. Until then, we would like to keep our toes intact and our applications stable. The configuration system we are devising gives us flexibility in our deployment options and/or a great way to save user state if we add a method to serialize data. Maybe we will look into that in my next post.

I've zipped and attached the example for anyone that wants it. At some point I will move sample code into a repository, but this will have to do for now.

Happy coding,
dinsdale
 

Attachments

  • LuaSolExample-Sources.zip
    194.8 KB · Views: 1
  • 1607493921273.png
    1607493921273.png
    44.2 KB · Views: 1
Last edited:
Top