Contents

Using lua as configuration parser

The more opportunity I have to work with lua, the bigger fan of it I become. It’s a great, well designed language which is very easy to embed.

I’ve been recently working on a small utility for myself. There’s always a problem of handling configuration in such projects. Usually, I’m going for a simple json or yaml or toml file which acts purely as key-value store but this time around I wanted to try something different.

The idea of using lua as a config backend is nothing new. It is successfully utilised in a number of well established projects. Most prominent one is probably neovim. I’ve never used lua like that myself before so, I wanted to explore this topic and see how it works on a smaller scale.

The application

Let’s start with some skeleton code so there’s something to work with. The configuration itself is just gonna be a key-value storage:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Config {
public:
  bool hasKey(std::string k) const {
    std::lock_guard<std::mutex> lock(m);
    std::cout << "Checking key " << k << std::endl;
    return data.find(k) != data.end();
  }

  std::optional<std::string> getKey(std::string k) {
    std::lock_guard<std::mutex> lock(m);
    std::cout << "Getting key " << k << std::endl;
    auto it = data.find(k);
    if (it != data.end()) {
      return it->second;
    }
    return std::nullopt;
  }

  void setKey(std::string k, std::string v) {
    std::lock_guard<std::mutex> lock(m);
    std::cout << "Setting key " << k << " -> " << v << std::endl;
    data[k] = v;
  }

private:
  mutable std::mutex m;
  std::map<std::string, std::string> data;
};

Nothing fancy. I’m keeping it simple here to avoid distractions. In real code, I’ve got support for values of different types, not only strings. I don’t want to delve into that here as type erasure and template specialisation obscure the main topic I want to focus on here. It’s also possible to implement support for recursive value types like lists or maps but again, it’s not relevant here.

I’m gonna model the application like so:

1
2
3
4
5
6
7
8
9
class Application {
public:
  Application(std::shared_ptr<Config> config) : config(config) {}

  void run(std::istream &s);

protected:
  std::shared_ptr<Config> config;
};

Application::run takes the stream containing the configuration data. I’ve done it this way so, it’s easier to test. I like to use streams for such purposes as it allows for using files, memory strings or other sources effortlessly and the interface remains flexible, generic, nice and simple.

I contained all the code related to lua in a simple class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class LuaScripting {
public:
  LuaScripting(std::shared_ptr<Config> config)
      : state(luaL_newstate(), lua_close), config{std::move(config)} {
    createConfigBindings();
  }

  void run(std::istream &is) {
    auto luaStr = std::string(std::istreambuf_iterator<char>(is), {});
    luaL_openlibs(state.get());
    if (luaL_dostring(state.get(), luaStr.c_str())) {
      std::cerr << "Error: " << lua_tostring(state.get(), -1) << std::endl;
    }
  }

  void createConfigBindings() {
    const struct luaL_Reg configLib[] = {
        {"hasKey",
         [](lua_State *L) -> int {
           auto self = static_cast<const LuaScripting *>(
               lua_touserdata(L, lua_upvalueindex(1)));
           int r = self->config->hasKey(luaL_checkstring(L, 1));
           lua_pushboolean(L, r);
           return 1;
         }},
        {"getKey",
         [](lua_State *L) -> int {
           auto self = static_cast<const LuaScripting *>(
               lua_touserdata(L, lua_upvalueindex(1)));
           if (auto r = self->config->getKey(luaL_checkstring(L, 1))) {
             lua_pushstring(L, r->c_str());
           } else {
             lua_pushnil(L);
           }
           return 1;
         }},
        {"setKey",
         [](lua_State *L) -> int {
           auto self = static_cast<const LuaScripting *>(
               lua_touserdata(L, lua_upvalueindex(1)));
           self->config->setKey(luaL_checkstring(L, 1), luaL_checkstring(L, 2));
           return 0;
         }},
        {nullptr, nullptr},
    };

    luaL_newlibtable(state.get(), configLib);
    lua_pushlightuserdata(state.get(), this);
    luaL_setfuncs(state.get(), configLib, 1);
    lua_setglobal(state.get(), "config");
  }

private:
  using State = std::unique_ptr<lua_State, decltype(&lua_close)>;

  State state;
  std::shared_ptr<Config> config;
};

That’s it! I’d argue that this may be less code required to process configuration than instantiation of any json/yaml parser along with all the glue code required and additional code needed to handle corner cases - like default values, switching back to environment in case the value is missing etc.

Usage

Let’s write some simple tests:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
TEST_F(ConfigTest, test_ifBasicOperationsOnConfigObjectWorkAsExpected) {
  std::stringstream s;
  s << "config.setKey('some key', 'some value')\n";
  app->run(s);
  ASSERT_TRUE(config->hasKey("some key"));
  ASSERT_EQ("some value", *config->getKey("some key"));
}


TEST_F(ConfigTest, test_ifSettingKeysToEnvironmentWorksAsExpected) {
  std::stringstream s;
  s << "home_path = os.getenv('HOME')\n"
       "config.setKey('home_path', home_path)\n";
  app->run(s);
  ASSERT_TRUE(config->hasKey("home_path"));
  ASSERT_NE(0, config->getKey("home_path")->size());
}

TEST_F(ConfigTest, test_ifSettingKeysDynamicallyWorksAsExpected) {
  std::stringstream s;
  s << "for i = 1, 10 do\n"
       "  config.setKey('key' .. i, 'value' .. i)\n"
       "end\n";
  app->run(s);
  for (int i = 1; i <= 10; ++i) {
    ASSERT_TRUE(config->hasKey("key" + std::to_string(i)));
    ASSERT_EQ("value" + std::to_string(i),
              *config->getKey("key" + std::to_string(i)));
  }
}

Having a real programming language as a back end for the configuration opens the door to a new set of possibilities. The data can be dynamic, it may be obtained from all sorts of sources and doesn’t have to be hard-coded. Relying on files like json/yaml would not provide such flexibility and most of the logic would have to be embedded in the application itself.

Conclusion

Working with lua is effortless. The stack based interface that it exposes via its C API is simple yet flexible. At first glance, it may seem a bit of an overkill to use a full scripting engine just for the sake of configuration parsing but there are undeniable benefits to it with a short list of drawbacks.

You get the format validation for free, since the configuration has to be a well formed lua code. There’s an undeniable flexibility that it gives you as well allowing to shift some of the native code to the config file itself. This flexibility might not always be desired and this is the only drawback I can see.

Referenced code can be found here.