From 28b585d92ba71eac9672425d1d532a4e206a6705 Mon Sep 17 00:00:00 2001 From: leftibot Date: Sat, 11 Apr 2026 11:28:15 -0600 Subject: [PATCH] Fix #571: Add per-instance IO redirection via set_print_handler/set_println_handler The print_string and println_string functions were previously registered as static functions writing directly to stdout, making it impossible to redirect ChaiScript output to custom destinations (e.g., GUI windows, loggers, or buffers). This moves their registration from Bootstrap::bootstrap() to ChaiScript_Basic::build_eval_system() as lambdas that dispatch through configurable std::function handlers, allowing each ChaiScript instance to independently redirect its output via set_print_handler() and set_println_handler(). Co-Authored-By: Claude Opus 4.6 (1M context) --- include/chaiscript/dispatchkit/bootstrap.hpp | 4 +- .../chaiscript/language/chaiscript_engine.hpp | 25 ++++++- unittests/compiled_tests.cpp | 65 +++++++++++++++++++ 3 files changed, 91 insertions(+), 3 deletions(-) diff --git a/include/chaiscript/dispatchkit/bootstrap.hpp b/include/chaiscript/dispatchkit/bootstrap.hpp index ba1de5ad..17965ea6 100644 --- a/include/chaiscript/dispatchkit/bootstrap.hpp +++ b/include/chaiscript/dispatchkit/bootstrap.hpp @@ -435,8 +435,8 @@ namespace chaiscript::bootstrap { m.add(fun(&Build_Info::compiler_id), "compiler_id"); m.add(fun(&Build_Info::debug_build), "debug_build"); - 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/set_println_handler 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 c9f22e58..8b3675d1 100644 --- a/include/chaiscript/language/chaiscript_engine.hpp +++ b/include/chaiscript/language/chaiscript_engine.hpp @@ -78,6 +78,14 @@ namespace chaiscript { std::map> m_namespace_generators; + std::function m_print_handler = [](const std::string &s) noexcept { + fwrite(s.c_str(), 1, s.size(), stdout); + }; + + std::function m_println_handler = [](const std::string &s) noexcept { + puts(s.c_str()); + }; + /// 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 { @@ -123,6 +131,9 @@ namespace chaiscript { add(t_lib); } + 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_println_handler(s); }), "println_string"); + 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"); @@ -236,7 +247,19 @@ namespace chaiscript { } public: - + + /// \brief Set a custom handler for print_string (no newline), used by ChaiScript's puts() + /// \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 Set a custom handler for println_string (with newline), used by ChaiScript's print() + /// \param[in] t_handler Function to call with the string to print (handler should append newline if desired) + void set_println_handler(std::function t_handler) { + m_println_handler = std::move(t_handler); + } + /// \brief Virtual destructor for ChaiScript virtual ~ChaiScript_Basic() = default; diff --git a/unittests/compiled_tests.cpp b/unittests/compiled_tests.cpp index 27a99585..d24590a9 100644 --- a/unittests/compiled_tests.cpp +++ b/unittests/compiled_tests.cpp @@ -1436,3 +1436,68 @@ TEST_CASE("Issue #421 - Switch with type_conversion does not compare destroyed o result })") == 0); } + +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 (for print_string, called by puts) + chai.set_print_handler([&captured_output](const std::string &s) { + captured_output += s; + }); + + // Set custom println handler (for println_string, called by print) + chai.set_println_handler([&captured_output](const std::string &s) { + captured_output += s + "\n"; + }); + + // 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 + 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 + 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_println_handler([&captured_output](const std::string &s) { + captured_output += s + "\n"; + }); + + 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_println_handler([&output1](const std::string &s) { output1 += s; }); + chai2.set_println_handler([&output2](const std::string &s) { output2 += s; }); + + chai1.eval("print(\"from1\")"); + chai2.eval("print(\"from2\")"); + + CHECK(output1 == "from1"); + CHECK(output2 == "from2"); +}