Skip to content
C++
Domain Track
Difficulty 3/5

Embedding Scripting Languages

C++ scripting integration — embedding Lua, embedding Python (pybind11/CPython API), sol2 for Lua, exposing C++ APIs to scripts, and hot reloading.

TL;DR

Embedding a scripting language lets users extend your app without recompiling. Lua is the standard for game/embedded use (small, fast, designed to be embedded). Python (via pybind11) is better for scientific/data tooling. Both allow hot-reload of scripts at runtime.


sol2 is a modern C++ wrapper around the Lua C API:

cpp
#include <sol/sol.hpp>

int main() {
    sol::state lua;
    lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string);

    // Execute a script string
    lua.script(R"(
        function greet(name)
            return "Hello, " .. name .. "!"
        end
    )");

    // Call Lua function from C++
    std::string result = lua["greet"]("World");
    std::println("{}", result);  // Hello, World!

    // Set/get global variables
    lua["player_speed"] = 5.0;
    double speed = lua["player_speed"];

    // Run a script file
    lua.script_file("game_logic.lua");
}

Exposing C++ Types to Lua (sol2)

cpp
struct Vec2 { float x, y; };

struct Player {
    Vec2        pos{};
    std::string name;
    int         health = 100;

    void move(float dx, float dy) { pos.x += dx; pos.y += dy; }
    bool is_alive() const { return health > 0; }
};

sol::state lua;
lua.open_libraries(sol::lib::base);

// Register Vec2
lua.new_usertype<Vec2>("Vec2",
    sol::constructors<Vec2(), Vec2(float, float)>(),
    "x", &Vec2::x,
    "y", &Vec2::y
);

// Register Player
lua.new_usertype<Player>("Player",
    "name",     &Player::name,
    "health",   &Player::health,
    "pos",      &Player::pos,
    "move",     &Player::move,
    "is_alive", &Player::is_alive
);

Player p;
p.name = "Hero";
lua["player"] = &p;  // expose existing C++ object to Lua

lua.script(R"(
    player:move(1.0, 2.0)
    print(player.name .. " at " .. player.pos.x .. "," .. player.pos.y)
)");

Calling C++ from Lua (Callbacks)

cpp
// Register a C++ function in Lua
lua.set_function("spawn_enemy", [](std::string type, int level) -> int {
    std::println("spawning {} at level {}", type, level);
    return 42;  // entity ID
});

lua.script(R"(
    local id = spawn_enemy("goblin", 3)
    print("spawned entity " .. id)
)");

// Register a table of functions (namespace-like)
lua["Engine"] = lua.create_table_with(
    "log",   [](std::string msg) { std::println("[LUA] {}", msg); },
    "time",  []() -> double { return /* get time */0.0; }
);

lua.script("Engine.log('hello from Lua')");

Hot Reloading Scripts

cpp
class ScriptEngine {
    sol::state lua_;
    std::filesystem::file_time_type last_modified_;
    std::filesystem::path script_path_;

public:
    void load(std::filesystem::path path) {
        script_path_ = path;
        reload();
    }

    void reload() {
        try {
            lua_.script_file(script_path_.string());
            last_modified_ = std::filesystem::last_write_time(script_path_);
            std::println("reloaded {}", script_path_.string());
        } catch (const sol::error& e) {
            std::println(stderr, "script error: {}", e.what());
        }
    }

    void tick() {
        // Check if script changed
        auto mtime = std::filesystem::last_write_time(script_path_);
        if (mtime != last_modified_) reload();

        // Call the Lua update function
        if (auto fn = lua_["update"]; fn.valid())
            fn(/* delta_time */ 0.016);
    }
};

Python via pybind11

For scientific/ML tooling where Python ecosystem is needed:

cpp
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
namespace py = pybind11;

// Expose a C++ function to Python
PYBIND11_MODULE(mymodule, m) {
    m.def("add", [](int a, int b) { return a + b; }, "Add two integers");

    py::class_<Player>(m, "Player")
        .def(py::init<std::string>())
        .def_readwrite("health", &Player::health)
        .def("move", &Player::move)
        .def("is_alive", &Player::is_alive);
}

// Then in Python:
// import mymodule
// p = mymodule.Player("Hero")
// p.move(1.0, 0.0)

CMake Setup

cmake
# Lua + sol2
find_package(Lua REQUIRED)
FetchContent_Declare(sol2 GIT_REPOSITORY https://github.com/ThePhD/sol2.git GIT_TAG v3.3.0)
FetchContent_MakeAvailable(sol2)

target_link_libraries(myapp PRIVATE ${LUA_LIBRARIES} sol2)
target_include_directories(myapp PRIVATE ${LUA_INCLUDE_DIR})

# pybind11
find_package(pybind11 CONFIG REQUIRED)
pybind11_add_module(mymodule src/bindings.cpp)
target_link_libraries(mymodule PRIVATE mylib)