C++ Embedded Script - CES is used to write template file for code generation.
During the conception of STIG, I was stuck on the general architecture of the code generator. The goal was generating C++ code that contains informations about classes to create the static type introspection.
The problem was « outsourcing » generation to allow frequent changes without re-compiling the tool each time. In addition the source of generation should be clear.
CES is the answer to this problem. This is a simple template-engine that parse template file with embedded Lua code and generate files.
You can easily connect your code to the Lua VM and add your own variables, methods and classes that will exposed to your template files.
Embedding script code into source code simplifies both reading and maintainability of template files.
- Source-Language independant (CES doesn't parse the source language, it can be whatever you want)
- Expose your C++ to Lua (You can access to your datas/methods from embedded-Lua script)
- Simple embedding syntax
CES uses CMake as build system The libraries Boost and Lua need to be present to build CES. If you plan to extend or modify CES, you may need to install SWIG.
Run the following code:
#include <iostream>
#include "ces/CES.hpp"
#include "ces/LuaVM.hpp"
#include "ces/Interpreter.hpp"
int main(int argc,char* argv[])
{
ces::LuaVM vm;
ces::Parser parser;
ces::Interpreter inter;
vm.open();
parser.parse("Hello@[for i=1,5 do]@ @i@,@[end]@ World");
inter.interpret(&vm, parser.ast(), std::cout);
vm.close();
return 0;
}
This should produce the following output:
Hello 1, 2, 3, 4, 5, World
Let's see how does it work. The following line open and close the Lua virtual machine.
vm.open();
...
vm.close();
LuaVM is necessary to run the embedded script writen in Lua.
Then the parser
parses (obviously) a chunk of CES Script:
parser.parse("Hello@[for i=1,5 do]@ @i@,@[end]@ World");
The details of the CES syntax are presented below.
And finally the interpreter
interprets the parser's AST (Abstract Syntax Tree), produces a Lua script, run this script using the Lua virtual machine vm
and stream the output to std::cout
.
The syntax of CES is very simple. The goal is to mix Lua in anything thus there are delimiters to mark when Lua code is starting and when anything is starting.
"Hello@[for i=1,5 do]@ @i@,@[end]@ World"
Lua code is delimited using @[
for starting a Lua block and ]@
for closing it.
The equivalent for the previous example would be something like
Anything("Hello")
Lua(for i=1,5 do)
@i@ Anything(",")
Lua(end)
Anything("World")
So, what are the @...@
?
The character @
alone is used to mark a block as output, it's the equivalent of std::cout
for CES.
The content of output blocks is evaluated and redirected to CES output (can be whichever stream you like: cout, file, buffer, ...).
To do this CES uses a custom method in Lua: CES.out
:
@i@ = CES.out(i)
In fact the anything blocks are also redirected to the CES output using CES.out
but not evaluated.
CES produces Lua scripts equivalent for each CES code interpreted:
"Hello@[for i=1,5 do]@ @i@,@[end]@ World"
// Produces
CES.out("Hello") // Hello
for i=1,5 do // @[for i=1,5 do]@
CES.out(i) // @i@
CES.out(",") // ,
end // @[end]@
CES.out(" World") // World
To make the most of CES you will need to bind you data members and method from C++ to Lua. Thanks to bindings you will be able to retrieve data from C++ in your CES file.
For example, if you are developping a RPG with procedural dungeons you may need to generate Level files (let say in XML). Your template CES file may looks like the following
<Dungeon id=@Dungeon.id@>
@[for i=0,table.getn(Dungeon.NPCs) do]@
<NPC type=@Dungeon.NPCS[i].type@>
...
</NPC>
@[end]@
</Dungeon>
In this example we need to access to the class Dungeon
and NPC
data members.
Unfortunatly CES can not expose these datas for you. The simplest way to do this is using SWIG. Documentation about SWIG and Lua can be found here