diff --git a/cheatsheet.md b/cheatsheet.md index d28241ec..cab7504f 100644 --- a/cheatsheet.md +++ b/cheatsheet.md @@ -876,5 +876,42 @@ m.is_type("MyClass") // true (checks the ChaiScript class name) * `from_json` converts a JSON string into its strongly typed (map, vector, int, double, string) representations * `to_json` converts a ChaiScript object (either a `Object` or one of map, vector, int, double, string) tree into its JSON string representation +## IO Redirection + +By default, ChaiScript's `print()` and `puts()` functions write to stdout. You can redirect +output on a per-instance basis by setting a single print handler. Both `println_string` +(used by `print()`) and `print_string` (used by `puts()`) dispatch through the same handler — +`println_string` simply appends a newline before calling it. + +```cpp +chaiscript::ChaiScript chai; + +// Redirect all output (print_string and println_string both use this handler) +chai.set_print_handler([](const std::string &s) { + my_log_window.append(s); +}); +``` + +This is useful for embedding ChaiScript in GUI applications, logging frameworks, or any +context where stdout is not the desired output destination. + +```cpp +// Example: capture all output to a string +std::string captured; +chai.set_print_handler([&captured](const std::string &s) { + captured += s; +}); + +chai.eval("print(42)"); // captured == "42\n" +chai.eval("puts(\"hi\")"); // captured == "42\nhi" +``` + +The print handler can also be set from within ChaiScript itself via `set_print_handler`: + +```chaiscript +// Redirect output from within a script +set_print_handler(fun(s) { my_custom_log(s) }) +``` + ## 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/chaiscript.hpp b/include/chaiscript/chaiscript.hpp index 1be2f0b4..de8c3288 100644 --- a/include/chaiscript/chaiscript.hpp +++ b/include/chaiscript/chaiscript.hpp @@ -830,7 +830,8 @@ namespace chaiscript { std::make_unique>(), std::move(t_modulepaths), std::move(t_usepaths), - std::move(t_opts)) { + std::move(t_opts), + std::find(t_lib_opts.begin(), t_lib_opts.end(), Library_Options::No_IO) != t_lib_opts.end()) { } }; } // namespace chaiscript diff --git a/include/chaiscript/dispatchkit/bootstrap.hpp b/include/chaiscript/dispatchkit/bootstrap.hpp index e5d18255..041af80e 100644 --- a/include/chaiscript/dispatchkit/bootstrap.hpp +++ b/include/chaiscript/dispatchkit/bootstrap.hpp @@ -437,10 +437,10 @@ namespace chaiscript::bootstrap { m.add(fun(&Build_Info::compiler_id), "compiler_id"); m.add(fun(&Build_Info::debug_build), "debug_build"); - if (!t_no_io) { - m.add(fun(&print), "print_string"); - m.add(fun(&println), "println_string"); - } + // print_string and println_string are registered in ChaiScript_Basic::build_eval_system() + // to support per-instance IO redirection via set_print_handler. + // When No_IO is set, the functions are still registered but the default handler + // is a no-op, so users can provide their own print handlers without any stdout output. m.add(dispatch::make_dynamic_proxy_function(&bind_function), "bind"); diff --git a/include/chaiscript/language/chaiscript_engine.hpp b/include/chaiscript/language/chaiscript_engine.hpp index 50dfb3c7..4afd0449 100644 --- a/include/chaiscript/language/chaiscript_engine.hpp +++ b/include/chaiscript/language/chaiscript_engine.hpp @@ -79,6 +79,8 @@ namespace chaiscript { std::map> m_namespace_generators; + std::function m_print_handler = [](const std::string &) noexcept {}; + /// 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) { try { @@ -119,11 +121,24 @@ namespace chaiscript { chaiscript::detail::Dispatch_Engine &get_eval_engine() noexcept { return m_engine; } /// Builds all the requirements for ChaiScript, including its evaluator and a run of its prelude. - void build_eval_system(const ModulePtr &t_lib, const std::vector &t_opts) { + void build_eval_system(const ModulePtr &t_lib, const std::vector &t_opts, const bool t_no_io = false) { if (t_lib) { add(t_lib); } + if (!t_no_io) { + m_print_handler = [](const std::string &s) noexcept { + fwrite(s.c_str(), 1, s.size(), stdout); + }; + } + + m_engine.add(fun([this](const std::string &s) { m_print_handler(s); }), "print_string"); + m_engine.add(fun([this](const std::string &s) { m_print_handler(s + "\n"); }), "println_string"); + + m_engine.add(fun([this](const std::function &t_handler) { + m_print_handler = t_handler; + }), "set_print_handler"); + 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"); @@ -256,7 +271,13 @@ namespace chaiscript { } public: - + + /// \brief Set a custom handler for print output, used by both print_string and println_string + /// \param[in] t_handler Function to call with the string to print + void set_print_handler(std::function t_handler) { + m_print_handler = std::move(t_handler); + } + /// \brief Virtual destructor for ChaiScript virtual ~ChaiScript_Basic() = default; @@ -268,7 +289,8 @@ namespace chaiscript { std::unique_ptr &&parser, std::vector t_module_paths = {}, std::vector t_use_paths = {}, - const std::vector &t_opts = chaiscript::default_options()) + const std::vector &t_opts = chaiscript::default_options(), + const bool t_no_io = false) : m_module_paths(ensure_minimum_path_vec(std::move(t_module_paths))) , m_use_paths(ensure_minimum_path_vec(std::move(t_use_paths))) , m_parser(std::move(parser)) @@ -303,7 +325,7 @@ namespace chaiscript { m_module_paths.insert(m_module_paths.begin(), dllpath + "/"); } #endif - build_eval_system(t_lib, t_opts); + build_eval_system(t_lib, t_opts, t_no_io); } #ifndef CHAISCRIPT_NO_DYNLOAD diff --git a/unittests/compiled_tests.cpp b/unittests/compiled_tests.cpp index 3cdef6cc..7b601b15 100644 --- a/unittests/compiled_tests.cpp +++ b/unittests/compiled_tests.cpp @@ -1360,17 +1360,26 @@ TEST_CASE("ChaiScript_Basic No_Stdlib option disables all standard library funct CHECK_THROWS(chai.eval("from_json(\"[1,2,3]\")")); } -TEST_CASE("ChaiScript_Basic No_IO option disables print functions") { +TEST_CASE("ChaiScript_Basic No_IO option uses null handler by default") { chaiscript::ChaiScript_Basic chai(chaiscript::Std_Lib::library({chaiscript::Library_Options::No_IO}), create_chaiscript_parser(), {}, {}, - {chaiscript::Options::No_Load_Modules, chaiscript::Options::No_External_Scripts}); + {chaiscript::Options::No_Load_Modules, chaiscript::Options::No_External_Scripts}, + true); - CHECK_THROWS(chai.eval("print_string(\"hello\")")); - CHECK_THROWS(chai.eval("println_string(\"hello\")")); + // print_string and println_string should still be available via the handler mechanism + // but the default handler is a no-op (no stdout output) + CHECK_NOTHROW(chai.eval("print_string(\"hello\")")); + CHECK_NOTHROW(chai.eval("println_string(\"hello\")")); CHECK(chai.eval("5 + 3") == 8); CHECK_NOTHROW(chai.eval("var v = Vector()")); + + // Users can set their own print handler even with No_IO + std::string captured; + chai.set_print_handler([&captured](const std::string &s) { captured += s; }); + chai.eval("print_string(\"redirected\")"); + CHECK(captured == "redirected"); } TEST_CASE("ChaiScript_Basic No_Prelude option disables prelude functions") { @@ -1431,10 +1440,18 @@ TEST_CASE("ChaiScript No_IO option via library options parameter") { {chaiscript::Options::No_Load_Modules, chaiscript::Options::No_External_Scripts}, {chaiscript::Library_Options::No_IO}); - CHECK_THROWS(chai.eval("print_string(\"hello\")")); - CHECK_THROWS(chai.eval("println_string(\"hello\")")); + // print_string and println_string remain available via the handler mechanism + // but the default handler is a no-op (no stdout output) + CHECK_NOTHROW(chai.eval("print_string(\"hello\")")); + CHECK_NOTHROW(chai.eval("println_string(\"hello\")")); CHECK(chai.eval("5 + 3") == 8); CHECK_NOTHROW(chai.eval("var v = Vector()")); + + // Users can override the null handler with their own + std::string captured; + chai.set_print_handler([&captured](const std::string &s) { captured += s; }); + chai.eval("print_string(\"redirected\")"); + CHECK(captured == "redirected"); } TEST_CASE("ChaiScript No_Prelude option via library options parameter") { @@ -1607,6 +1624,83 @@ TEST_CASE("Issue 625: function_less_than strict-weak ordering with different ari CHECK(chai.eval("overloaded(3, 2.0)") == 5); } +TEST_CASE("IO redirection with set_print_handler") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + std::string captured_output; + + // Set custom print handler — both print_string and println_string dispatch through it + chai.set_print_handler([&captured_output](const std::string &s) { + captured_output += s; + }); + + // Test that puts() uses the custom handler + captured_output.clear(); + chai.eval("puts(\"hello\")"); + CHECK(captured_output == "hello"); + + // Test that print() uses the custom handler (println_string appends newline before calling handler) + captured_output.clear(); + chai.eval("print(\"world\")"); + CHECK(captured_output == "world\n"); + + // Test that print_string() directly uses the custom handler + captured_output.clear(); + chai.eval("print_string(\"direct\")"); + CHECK(captured_output == "direct"); + + // Test that println_string() directly uses the custom handler with newline + captured_output.clear(); + chai.eval("println_string(\"direct_ln\")"); + CHECK(captured_output == "direct_ln\n"); +} + +TEST_CASE("IO redirection captures numeric output") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + std::string captured_output; + chai.set_print_handler([&captured_output](const std::string &s) { + captured_output += s; + }); + + chai.eval("print(42)"); + CHECK(captured_output == "42\n"); +} + +TEST_CASE("IO redirection different instances are independent") { + chaiscript::ChaiScript_Basic chai1(create_chaiscript_stdlib(), create_chaiscript_parser()); + chaiscript::ChaiScript_Basic chai2(create_chaiscript_stdlib(), create_chaiscript_parser()); + + std::string output1; + std::string output2; + + chai1.set_print_handler([&output1](const std::string &s) { output1 += s; }); + chai2.set_print_handler([&output2](const std::string &s) { output2 += s; }); + + chai1.eval("print(\"from1\")"); + chai2.eval("print(\"from2\")"); + + CHECK(output1 == "from1\n"); + CHECK(output2 == "from2\n"); +} + +TEST_CASE("set_print_handler accessible from ChaiScript") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + auto captured = std::make_shared(); + chai.add(chaiscript::fun([captured](const std::string &s) { *captured += s; }), "test_output_sink"); + + // Set the print handler from within ChaiScript + chai.eval("set_print_handler(fun(s) { test_output_sink(s) })"); + + chai.eval("print(\"from_script\")"); + CHECK(*captured == "from_script\n"); + + captured->clear(); + chai.eval("puts(\"no_newline\")"); + CHECK(*captured == "no_newline"); +} + // Regression test: push_back() on script-created vector has no effect when // vector_conversion is in effect. The bug occurs because dispatch selects // the C++ push_back for the converted type over the built-in one, operating