primer::api::vfs helps to create a custom filesystem
exposed to lua.
lua provides some core
functionality to allow scripts to load modules from the filesystem, through
three functions loadfile,
dofile, and require. These aren't part of the package module, they are part of the
base library, and the package search path is something that can be configured
during the process of installing lua.
In an embedded lua application, it's likely that you'll want to sandbox this and prevent lua from directly accessing and loading things from the filesystem.
primer::api::vfs helps to simplify this. Instead of
implementing three very similar functions, to use primer::api::vfs,
you only implement one of them. You do this by creating a class satisfying
the "VFS Provider" concept.
Then, you make your class derive from the primer::api::vfs
class template using the CRTP style. This basically performs "dependency-injection",
so that the generic implementations of dofile,
loadfile, and require can access your load function and do the right thing,
and brings in the boiler-plate which makes it a proper API feature. primer::api::vfs has no data members and no nontrivial
intialization, but your "VFS Provider" might.
A VFS Provider is expected to provide a member function load:
expected<void> load(lua_State *, const std::string & path);
which attempts to load the module corresponding to path
onto the stack, using e.g. luaL_loadbuffer.
It should then return "ok", i.e., no error. If it cannot, it
should instead return an error message. For instance you might use the
pre-formatted message primer::error::module_not_found.
// Model of vfs provider concept struct my_files : primer::api::vfs<my_files> { using map_t = std::map<std::string, std::string>; map_t files_; explicit my_files(map_t m) : files_(std::move(m)) {} primer::expected<void> load(lua_State * L, const std::string & path) { auto it = files_.find(path); if (it != files_.end()) { const std::string & chunk = it->second; luaL_loadbuffer(L, chunk.c_str(), chunk.size(), path.c_str()); return {}; } else { return primer::error::module_not_found(path); } } }; // Example api::base object struct test_api : primer::api::base<test_api> { lua_raii L_; API_FEATURE(primer::api::sandboxed_basic_libraries, libs_); API_FEATURE(my_files, vfs_); API_FEATURE(primer::api::print_manager, print_man_); test_api() : L_() , vfs_{ {{"foo", "return {}"}, {"bar", "local function baz() return 5 end; return { baz = baz }"}}} { this->initialize_api(L_); } std::string save() { std::string result; this->persist(L_, result); return result; } void restore(const std::string & buffer) { this->unpersist(L_, buffer); } }; UNIT_TEST(api_vfs) { std::string buffer; table_summary summary; const char * script = "assert(type(5) == 'number') \n" "assert(type('foo') == 'string') \n" "assert(type(loadfile 'foo') == 'function') \n" "assert(type(dofile 'foo') == 'table') \n" " \n" "local foo = require 'foo' \n" "assert(type(foo) == 'table') \n" "assert(type(bar) == 'nil') \n" "local bar = require 'bar' \n" "assert(5 == bar.baz()) \n" "assert(not pcall(require, 'baz')) \n" " \n" "assert(bar == require 'bar') \n" "assert(bar ~= dofile 'bar') \n" " " "\n"; { test_api a; lua_State * L = a.L_; TEST_LUA_OK(L, luaL_loadstring(L, script)); TEST_LUA_OK(L, lua_pcall(L, 0, 0, 0)); buffer = a.save(); summary = get_global_table_summary(L); } { test_api a; lua_State * L = a.L_; TEST_LUA_OK(L, luaL_loadstring(L, script)); TEST_LUA_OK(L, lua_pcall(L, 0, 0, 0)); a.restore(buffer); auto summary2 = get_global_table_summary(L); // TEST(check_tables_match(summary, summary2), // "expected global tables to match!"); if (!check_tables_match(summary, summary2)) { std::cerr << "WARN: Global table mismatch!\n"; } TEST_LUA_OK(L, luaL_loadstring(L, script)); TEST_LUA_OK(L, lua_pcall(L, 0, 0, 0)); } }
TODO