From 1cbbba38f991e25027c52c4243afa1794aca59db Mon Sep 17 00:00:00 2001 From: leftibot Date: Wed, 15 Apr 2026 16:25:09 -0600 Subject: [PATCH] Fix #116: Add set_file_reader callback for custom file loading (#683) Add a customizable file reader callback to ChaiScript_Basic, following the same pattern as set_print_handler. When set, the callback is invoked instead of the default filesystem read, enabling use cases like encrypted files, in-memory virtual filesystems, or platform-specific file access (e.g., Android assets). The callback is settable from both C++ and ChaiScript. Co-authored-by: leftibot Co-authored-by: Claude Opus 4.6 (1M context) --- cheatsheet.md | 37 ++++++++++++++++++ .../chaiscript/language/chaiscript_engine.hpp | 21 +++++++++- releasenotes.md | 1 + unittests/compiled_tests.cpp | 38 +++++++++++++++++++ 4 files changed, 96 insertions(+), 1 deletion(-) diff --git a/cheatsheet.md b/cheatsheet.md index 4f5f0ff5..bb704105 100644 --- a/cheatsheet.md +++ b/cheatsheet.md @@ -1119,6 +1119,43 @@ The print handler can also be set from within ChaiScript itself via `set_print_h set_print_handler(fun(s) { my_custom_log(s) }) ``` +## Custom File Loading + +By default, ChaiScript reads files from the filesystem when `eval_file()` or `use()` is called. +You can override this behavior on a per-instance basis by setting a custom file reader callback. +This follows the same pattern as `set_print_handler` and enables use cases such as encrypted +script files, in-memory virtual filesystems, or platform-specific file access (e.g., Android assets). + +```cpp +chaiscript::ChaiScript chai; + +// Provide scripts from an in-memory map instead of the filesystem +std::map virtual_fs = { + {"init.chai", "var x = 42"}, + {"utils.chai", "def add(a, b) { a + b }"} +}; + +chai.set_file_reader([&virtual_fs](const std::string &filename) -> std::string { + const auto it = virtual_fs.find(filename); + if (it != virtual_fs.end()) { + return it->second; + } + throw chaiscript::exception::file_not_found_error(filename); +}); + +chai.eval_file("init.chai"); // evaluates "var x = 42" +chai.use("utils.chai"); // evaluates "def add(a, b) { a + b }" +``` + +The file reader can also be set from within ChaiScript itself via `set_file_reader`: + +```chaiscript +// Override file loading from within a script +set_file_reader(fun(filename) { return my_custom_read(filename) }) +``` + +When no custom file reader is set, ChaiScript uses its built-in filesystem reader. + ## Extras ChaiScript itself does not provide a link to the math functions defined in ``. You can either add them yourself, or use the [ChaiScript_Extras](https://github.com/ChaiScript/ChaiScript_Extras) helper library. (Which also provides some additional string functions.) diff --git a/include/chaiscript/language/chaiscript_engine.hpp b/include/chaiscript/language/chaiscript_engine.hpp index de5e4b2f..a3c25639 100644 --- a/include/chaiscript/language/chaiscript_engine.hpp +++ b/include/chaiscript/language/chaiscript_engine.hpp @@ -80,6 +80,7 @@ namespace chaiscript { std::map> m_namespace_generators; std::function m_print_handler = [](const std::string &) noexcept {}; + std::function m_file_reader; /// Evaluates the given string in by parsing it and running the results through the evaluator Boxed_Value do_eval(const std::string &t_input, const std::string &t_filename = "__EVAL__", bool /* t_internal*/ = false) { @@ -139,6 +140,10 @@ namespace chaiscript { m_print_handler = t_handler; }), "set_print_handler"); + m_engine.add(fun([this](const std::function &t_reader) { + m_file_reader = t_reader; + }), "set_file_reader"); + m_engine.add(fun([this]() { m_engine.dump_system(); }), "dump_system"); m_engine.add(fun([this](const Boxed_Value &t_bv) { m_engine.dump_object(t_bv); }), "dump_object"); m_engine.add(fun([this](const Boxed_Value &t_bv, const std::string &t_type) { return m_engine.is_type(t_bv, t_type); }), "is_type"); @@ -243,7 +248,15 @@ namespace chaiscript { } /// Helper function for loading a file - static std::string load_file(const std::string &t_filename) { + std::string load_file(const std::string &t_filename) const { + if (m_file_reader) { + return m_file_reader(t_filename); + } + + return load_file_default(t_filename); + } + + static std::string load_file_default(const std::string &t_filename) { std::ifstream infile(t_filename.c_str(), std::ios::in | std::ios::ate | std::ios::binary); if (!infile.is_open()) { @@ -285,6 +298,12 @@ namespace chaiscript { m_print_handler = std::move(t_handler); } + /// \brief Set a custom handler for reading files, used by eval_file, use, and internal_eval_file + /// \param[in] t_reader Function to call with the filename, returning the file contents as a string + void set_file_reader(std::function t_reader) { + m_file_reader = std::move(t_reader); + } + /// \brief Virtual destructor for ChaiScript virtual ~ChaiScript_Basic() = default; diff --git a/releasenotes.md b/releasenotes.md index a622566e..f1b2b53a 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -4,6 +4,7 @@ Current Version: 6.1.1 ### Changes since 6.1.0 + * Add `set_file_reader` callback for custom file loading #116 — allows overriding how `eval_file` and `use` read files * Handle the returning of `&` to `*` types. This specifically comes up with `std::vector` and similar containers * Update CMake to use `LIBDIR` instead of `lib` #502 by @guoyunhe * Add documentation for installing ChaiScript with vcpkg #500 by @grdowns diff --git a/unittests/compiled_tests.cpp b/unittests/compiled_tests.cpp index 680535ce..4ac537ca 100644 --- a/unittests/compiled_tests.cpp +++ b/unittests/compiled_tests.cpp @@ -1822,6 +1822,44 @@ TEST_CASE("eval_error with AST_Node_Trace call stack compiles in C++20") { } } +TEST_CASE("Test set_file_reader from C++ land") { + chaiscript::ChaiScript chai; + chai.set_file_reader([](const std::string &) { + return std::string("var file_reader_test_val = 42"); + }); + chai.eval_file("nonexistent_file.chai"); + CHECK(chai.eval("file_reader_test_val") == 42); +} + +TEST_CASE("Test set_file_reader from ChaiScript land") { + chaiscript::ChaiScript chai; + chai.set_file_reader([](const std::string &) { + return std::string("var from_custom_reader = true"); + }); + chai.eval("set_file_reader(fun(filename) { return \"var from_chai_reader = true\"; })"); + chai.eval_file("any_file.chai"); + CHECK(chai.eval("from_chai_reader") == true); +} + +TEST_CASE("Test set_file_reader receives correct filename") { + chaiscript::ChaiScript chai; + std::string captured_filename; + chai.set_file_reader([&captured_filename](const std::string &t_filename) { + captured_filename = t_filename; + return std::string("var dummy = 1"); + }); + chai.eval_file("my_special_file.chai"); + CHECK(captured_filename == "my_special_file.chai"); +} + +TEST_CASE("Test use with set_file_reader") { + chaiscript::ChaiScript chai; + chai.set_file_reader([](const std::string &) { + return std::string("var use_reader_val = 99"); + }); + chai.use("virtual_file.chai"); + CHECK(chai.eval("use_reader_val") == 99); +} TEST_CASE("Nested namespaces via register_namespace with :: separator") { chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser());