Skip to content

Commit 1cbbba3

Browse files
leftibotclaude
andauthored
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 <leftibot@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent bb06919 commit 1cbbba3

4 files changed

Lines changed: 96 additions & 1 deletion

File tree

cheatsheet.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,6 +1119,43 @@ The print handler can also be set from within ChaiScript itself via `set_print_h
11191119
set_print_handler(fun(s) { my_custom_log(s) })
11201120
```
11211121

1122+
## Custom File Loading
1123+
1124+
By default, ChaiScript reads files from the filesystem when `eval_file()` or `use()` is called.
1125+
You can override this behavior on a per-instance basis by setting a custom file reader callback.
1126+
This follows the same pattern as `set_print_handler` and enables use cases such as encrypted
1127+
script files, in-memory virtual filesystems, or platform-specific file access (e.g., Android assets).
1128+
1129+
```cpp
1130+
chaiscript::ChaiScript chai;
1131+
1132+
// Provide scripts from an in-memory map instead of the filesystem
1133+
std::map<std::string, std::string> virtual_fs = {
1134+
{"init.chai", "var x = 42"},
1135+
{"utils.chai", "def add(a, b) { a + b }"}
1136+
};
1137+
1138+
chai.set_file_reader([&virtual_fs](const std::string &filename) -> std::string {
1139+
const auto it = virtual_fs.find(filename);
1140+
if (it != virtual_fs.end()) {
1141+
return it->second;
1142+
}
1143+
throw chaiscript::exception::file_not_found_error(filename);
1144+
});
1145+
1146+
chai.eval_file("init.chai"); // evaluates "var x = 42"
1147+
chai.use("utils.chai"); // evaluates "def add(a, b) { a + b }"
1148+
```
1149+
1150+
The file reader can also be set from within ChaiScript itself via `set_file_reader`:
1151+
1152+
```chaiscript
1153+
// Override file loading from within a script
1154+
set_file_reader(fun(filename) { return my_custom_read(filename) })
1155+
```
1156+
1157+
When no custom file reader is set, ChaiScript uses its built-in filesystem reader.
1158+
11221159
## Extras
11231160
ChaiScript itself does not provide a link to the math functions defined in `<cmath>`. 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.)
11241161

include/chaiscript/language/chaiscript_engine.hpp

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ namespace chaiscript {
8080
std::map<std::string, std::function<Namespace &()>> m_namespace_generators;
8181

8282
std::function<void(const std::string &)> m_print_handler = [](const std::string &) noexcept {};
83+
std::function<std::string(const std::string &)> m_file_reader;
8384

8485
/// Evaluates the given string in by parsing it and running the results through the evaluator
8586
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 {
139140
m_print_handler = t_handler;
140141
}), "set_print_handler");
141142

143+
m_engine.add(fun([this](const std::function<std::string(const std::string &)> &t_reader) {
144+
m_file_reader = t_reader;
145+
}), "set_file_reader");
146+
142147
m_engine.add(fun([this]() { m_engine.dump_system(); }), "dump_system");
143148
m_engine.add(fun([this](const Boxed_Value &t_bv) { m_engine.dump_object(t_bv); }), "dump_object");
144149
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 {
243248
}
244249

245250
/// Helper function for loading a file
246-
static std::string load_file(const std::string &t_filename) {
251+
std::string load_file(const std::string &t_filename) const {
252+
if (m_file_reader) {
253+
return m_file_reader(t_filename);
254+
}
255+
256+
return load_file_default(t_filename);
257+
}
258+
259+
static std::string load_file_default(const std::string &t_filename) {
247260
std::ifstream infile(t_filename.c_str(), std::ios::in | std::ios::ate | std::ios::binary);
248261

249262
if (!infile.is_open()) {
@@ -285,6 +298,12 @@ namespace chaiscript {
285298
m_print_handler = std::move(t_handler);
286299
}
287300

301+
/// \brief Set a custom handler for reading files, used by eval_file, use, and internal_eval_file
302+
/// \param[in] t_reader Function to call with the filename, returning the file contents as a string
303+
void set_file_reader(std::function<std::string(const std::string &)> t_reader) {
304+
m_file_reader = std::move(t_reader);
305+
}
306+
288307
/// \brief Virtual destructor for ChaiScript
289308
virtual ~ChaiScript_Basic() = default;
290309

releasenotes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Current Version: 6.1.1
44

55
### Changes since 6.1.0
66

7+
* Add `set_file_reader` callback for custom file loading #116 — allows overriding how `eval_file` and `use` read files
78
* Handle the returning of `&` to `*` types. This specifically comes up with `std::vector<int *>` and similar containers
89
* Update CMake to use `LIBDIR` instead of `lib` #502 by @guoyunhe
910
* Add documentation for installing ChaiScript with vcpkg #500 by @grdowns

unittests/compiled_tests.cpp

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1822,6 +1822,44 @@ TEST_CASE("eval_error with AST_Node_Trace call stack compiles in C++20") {
18221822
}
18231823
}
18241824

1825+
TEST_CASE("Test set_file_reader from C++ land") {
1826+
chaiscript::ChaiScript chai;
1827+
chai.set_file_reader([](const std::string &) {
1828+
return std::string("var file_reader_test_val = 42");
1829+
});
1830+
chai.eval_file("nonexistent_file.chai");
1831+
CHECK(chai.eval<int>("file_reader_test_val") == 42);
1832+
}
1833+
1834+
TEST_CASE("Test set_file_reader from ChaiScript land") {
1835+
chaiscript::ChaiScript chai;
1836+
chai.set_file_reader([](const std::string &) {
1837+
return std::string("var from_custom_reader = true");
1838+
});
1839+
chai.eval("set_file_reader(fun(filename) { return \"var from_chai_reader = true\"; })");
1840+
chai.eval_file("any_file.chai");
1841+
CHECK(chai.eval<bool>("from_chai_reader") == true);
1842+
}
1843+
1844+
TEST_CASE("Test set_file_reader receives correct filename") {
1845+
chaiscript::ChaiScript chai;
1846+
std::string captured_filename;
1847+
chai.set_file_reader([&captured_filename](const std::string &t_filename) {
1848+
captured_filename = t_filename;
1849+
return std::string("var dummy = 1");
1850+
});
1851+
chai.eval_file("my_special_file.chai");
1852+
CHECK(captured_filename == "my_special_file.chai");
1853+
}
1854+
1855+
TEST_CASE("Test use with set_file_reader") {
1856+
chaiscript::ChaiScript chai;
1857+
chai.set_file_reader([](const std::string &) {
1858+
return std::string("var use_reader_val = 99");
1859+
});
1860+
chai.use("virtual_file.chai");
1861+
CHECK(chai.eval<int>("use_reader_val") == 99);
1862+
}
18251863
TEST_CASE("Nested namespaces via register_namespace with :: separator") {
18261864
chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser());
18271865

0 commit comments

Comments
 (0)